C++ Primer笔记6---chapter6易错点

Posted Ston.V

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer笔记6---chapter6易错点相关的知识,希望对你有一定的参考价值。

1.局部静态变量:在第一次被执行时被初始化,知道程序终止时才被销毁;对于类的static成员变量,其对象的所有实例都共用这一个成员变量;局部静态变量默认初始值为0

//其中该ctr表示自己被调用了多少次
size_t count_calls(){
    static size_t ctr=0;
    return ++ctr;
}

2.函数声明没有函数体,所以实际上形参的名字可以省略,当然加上更好

一般习惯声明放在头文件中,定义放在源文件中,含有函数声明的头文件应该被包含到定义函数的源文件中去,使用宏避免重复包含头文件

3.参数传递:传引用调用 & 传值调用

当函数无序改变引用形参的值,最好将其声明为常量引用,此时传递常量或者非常量对象(会忽略掉他的顶层const---所指的对象为常量,如const int a; const int *p;)都可以;但是也因此要注意重复定义的问题,不妨你不修改他时就声明为const就可以了

void fcn(const int i){   //fcn能够读取i,但是不能写i
    xxx
}

void fcn(int i){        //错误:重复定义,顶层const会被忽略,所以二者能够传入的参数其实是一样的
    xxx
}

4.如果调用参数为引用的函数,只能使用为int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象作为参数

如果调用参数为常量引用的函数,可以使用字面值作为参数

因此我们声明函数的形参尽量使用常量引用,这样就能够避免因为直接用字面值作为实参导致的错误(使用引用的原因是避免了拷贝,节省资源,有的对象甚至不支持拷贝)

5.数组形参的几种写法:

1)使用标记指定数组长度(如字符串的空字符)

2)使用标准库规范

void print(const int *beg,const int * end){
    while(beg!=end)
        cout<<*beg++<<endl;
}

int j[2]={0,1};
print(begin(j),end(j));

3)显示传递一个表示数组大小的形参(C中最常用),这个对应的实参可以用sizeof表示,也可以用end(j)-begin(j)表示

6.对于二维数组的形参,在C++中二维数组实际上是数组的数组,我们传的应该是数组的第一个元素的地址,所以形参应当是指向数组的指针,注意不要写成指针数组的形式

//其中二维大小不能忽略。一维可以忽略,而且即使形参指定了一维也不影响实参该怎么传怎么传
void print(int (*matrix)[10],int rowSize){
    xxx
}

//等价于
void print(int matrix[][10],int rowSize){
    xxx
}

7.作为main函数的实参,其中argv[0]保存的是程序的名字,实际的参数要从argv[1]开始

int main(int argc,char *argv[]){
    xxx
}

8.含有可变形参的函数

1)如果所有实参类型相同,可以传递一个名为initializer_list的标准库类型,这个initializer_list中的对象都是常量值,不可修改

//比如错误处理的例子
void error_msg(ErrorCode e,initializer_list<string> il)
{
    cout<<e.msg()<<endl;
    for(const auto &elem : il)
        cout<<elem<<" ";
    cout<<endl;
}


//传递参数数目不同,但是类型都必须一致
case 1:
    error_msg(ErrorCode(1),{"Functionx ","hello","world"});break;
case 2:
    error_msg(ErrorCode(2),{"Functionx ","fuck"});break;
xxxxxx

2)如果实参类型不同,可以编写可变参数模板

3)可以使用省略符,来传递可变数量的实参,这种功能一般只用于与C函数交互的接口程序,省略符只能出现在形参列表的最后一个位置,prinf函数实现就是用的这种可变形参

void foo(fmt,...);
void foo(...);

原理:利用参数压入栈从右往左的顺序(先入栈的地址更大),可以反推出各个参数的地址以便获取具体的参数值,往往需要有这么一个fmt来表示后面的各个参数的类型,同样的为了利于推算各个参数的地址可以使用标准库中的va_list

具体分析见这位老哥的博客:https://www.cnblogs.com/li-ning/p/9489986.html

9.含有return语句的循环后面也应该有一条return语句,如果没有的话程序就是错误的,很多编译器可能都无法发现此类错误

10.引用返回左值,我们能够给他赋值

char &get_val(string str,string::size_type ix){
    return str[ix];        //假定索引有效
}

int main(){
    string s("hello world");
    get_val(s,1)='a';            //返回的引用可以做左值,能够出现在赋值运算左边
    return 0;
}

11.对于main函数的返回值,在stdlib中有定义EXIT_SUCCESS (0)和EXIT_FAILURE (非0)来用于返回,以表示执行成功或失败,这些都是预处理量。

12.返回数组指针,可以使用typedef来简化写法

typedef int arrT[10];    //arrt是一个类型别名,表示含有10个整数的数组

using arrT = int[10];    //等价写法

arrT *func(int i);       //func接受一个整形实参,返回一个包含10个整数的数组

声明一个返回数组指针的函数

//首先区别数组指针和指针数组
int arr[10];
int *p1[10];    //指针数组
int (*p2)[10] = &arr ;    //数组指针


//具体的函数返回数组指针的例子,很繁琐
int (*func(int i))[10];


//C++ 11中可以使用尾置返回类型,返回类型写到后面了,前面用auto来表示
auto func(int) -> int(*)[10];

//如果知道函数返回的指针指向哪个数组,可以使用decltype.
//decltype(odd)表示的类型是一个指针,这个指针指向的对象与odd一样;但是decltype并不把数组类型转换为对应的指针,所以还要加上一个*(我的理解:decltype返回的类型就是数组,然后就想平时声明指针那样多加个*即可)
int odd[] ={1,3,5,7,9};
int even[]={2,4,6,8};
decltype(odd) * arrPtr(int i){
    return (i%2)? &odd : &even;
}

13.注意:不要返回局部对象的引用或指针,你返回时候人家的生命周期已经结束了

14.函数重载:在同一个作用域内函数名字相同但是形参列表不同;

不允许两个函数除了返回类型外其他所有要素都相同

重载和const形参,会忽略顶层const(自身是个常量),不会忽略底层const(所指的对象是常量)

一个拥有顶层const的形参和没有的顶层const的形参无法区分开,不可重载

//会忽略顶层const(左底右顶),导致重载失败
Record lookup(Phone);
Record lookup(const Phone);    //重复声明了Record lookup(Phone)

Record lookup(Phone *);
Record lookup(Phone * const);  //重复声明的Record lookup(Phone*);

如果形参是引用或者指针,通过区分其指向的对象是常量还是非常量可以实现函数重载,此时const是底层的

//以下都是ok的
Record lookup(Account &);
Record lookup(const Account &);


Record lookup(Account *);
Record lookup(const Account *);

注意:一个对象本身是常量是顶层const,指针指向的对象是常量是底层const

如果我们参数是一个对象,对于重载他是常量还是非常量是分不清的;但是搞一个指针或者引用指向他,这时候这个对象是常量还是非常量就分的清了(从顶层const变为底层const)

15.重载和作用域:注意在九层作用域声明函数,会隐藏掉外层作用域中的同名函数

string read();
void print(const string &);
void print(double);
void fooBar(int i){
    bool read=false;
    //错误:上一句中,布尔变量read隐藏了函数read,导致这里read实际上是布尔变量
    string s=read();  

    //强行声明一个新的print函数,会隐藏掉外层的两个print函数声明,下面调用的实际上是这个
    void print(int);  
    print("value");   //错误:print(const string &)被隐藏了
    print(i);         //正确:当前print(int)可见
    print(3.15);      //正确:调用的print(int),实际执行的是print(3);
}

16.默认实参:

1)定义,形如下,有默认值的形参的右侧的形参必须都有形参,使用时如果想要使用默认形参可以直接省略,所以一般把常用默认值的形参放在参数列表靠后,避免形参实参匹配出错

typedef string::size_type sz;
string screen(sz ht=24,sz wd=80,char bg=' ');

2)对于声明,多次声明同一个函数合法,但是在给定作用域中想要修改默认实参值的声明不允许,想要给没有默认值的形参添加默认值的声明允许,但是仍要注意有默认实参的形参右侧参数必须都有默认值

3)局部变量不可以作为默认实参,除此之晚,只能类型相符合/能转化的表达式均可。全局变量作默认实参时,若此全局变量的值改变,形参的默认值也会改变

//下面三个的声明必须在函数之外
sz wd=80;
char def=' ';
sz ht();

string screen(sz=ht(), sz=wd, char =def );
string window=sreen();    //实际调用screen(ht(),80,' ');

void f2(){
    def='*';    //改变默认实参的值
    sz wd=100;  //隐藏了外层的wd,但是没有改变默认值
    window = screen();    //实际调用screen(ht(),80,'*');
}

17.内联函数&constexpr函数

内联函数:在函数返回值前加上inline声明即可,在编译过程中会将函数展开,从而能够避免函数的调用开销,

constexpr函数:函数的返回值和形参都得是字面值类型,且函数体只能有一条return语句。编译器对constexpr函数的调用替换为其结果值,且被隐式的指定为内联函数

对于给定的内联函数和constexpr函数,他的多个定义必须完全一致,因此他们通常定义在头文件中。

18.调试

可以使assert函数(cassert头文件)和NDEBUG宏搭配来输出调试信息,在发布版本中#define NDEBUG即可让这些assert失效。assert应当只被用于调试。

assert(expr):当expr为0,输出信息并终止程序;当expr为1,啥也不干

一些编译器为我们定义的用于调试的名字:__func__(函数名),__FILE__(文件名), __LINE__(当前行号), __TIME__(文件编译时间), __DATE__(文件编译日期)

19.(重载)函数匹配过程(面试考过):

1)选定本次调用对应的重载函数集,集合中的函数称为候选函数,候选函数的两个特征:一是与被调用函数名相同,二是其声明在调用点可见

2)考察本次调用的实参,然后从候选函数中选出能够这组实参调用的函数,这些函数称为可行函数,可行函数的两个特征:一是形参数目与实参数目相同(如果函数有默认实参,则传入实参可能比实际实参少),二是每个实参类型与形参类型相同或者能实参转化为形参类型

3)从可行函数中找出与本次调用最匹配的函数,逐一检查实参,实参类型与形参类型越接近,匹配程度越高

4)最终根据3)选出唯一一个函数:该函数每个实参的匹配都不劣于其他可行函数需要的匹配,且至少有一个实参的匹配优于其他可行函数提供的匹配。

5)若1)2)没有符合要求多的函数,则报告无匹配函数的错误;若4)中没能确定唯一的函数,则因为调用具有二义性二拒绝其请求。

20.函数指针:

//1.原始的定义
bool (*pf)(int a,int b);
bool func(int a,int b);
pf=func;    //pf指向func
pf=&func    //等价,&符号可以省略


//2.重载函数指针
void ff(int *);
void ff(unsigned int);

void (*pf1)(unsigned int)=ff    //pf1指向ff(unsigned)
void (*pf2)(int)=ff             //错误:没有一个形参匹配的
double (*pf3)(int *)=ff         //错误:返回类型不匹配


//3.函数指针可以做形参,也可以做返回值,可以使用别名来简化

//3.1作形参
void test(func,int x);    //函数类型会自动转化为函数指针类型,作为实参也会自动转化为指针类型
void test(&func,int x);   //同上,等价

//使用别名
typedef bool(*pf4)(int a,int b);
typedef decltype(func) *pf4    //同上,等价

void test(pf4,int x)

//3.2作返回值
using F =bool(int ,int);    //函数类型
using PF=bool(*)(int ,int); //函数指针类型!!!!!!!!!!!!!!!一定要注意好这个类型

PF f1(int);    //f1返回函数指针类型
F f1(int);     //错误:函数类型不能作为返回值类型
F* f1(int);    //正确,等价

//直接声明 & 使用尾置返回类型的方式
int (*f1(int))(int ,int);
auto f1(int) -> int(*)(int ,int);

 

 

 

 

 

 

以上是关于C++ Primer笔记6---chapter6易错点的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer笔记16---chapter13 代码实例

C++ Primer笔记16---chapter13 代码实例

C++ Primer笔记15---chapter13 拷贝控制2

C++ Primer笔记15---chapter13 拷贝控制2

C++ C++ Primer 基础知识笔记

C++ Primer Plus读书笔记