《C++Primer》
- 语句
- 函数
语句
if else
就C++而言,规定else
与离它最近的尚未匹配的if
匹配,从而消除程序的二义性,所以最好的处理方法就是养成习惯在if else
后面加一个花括号swirch
搭配case
关键字使用,case
关键字和它对应的值一起被称为case
标签,case标签
必须是整型常量表达式
char ch = getVal();
int ival = 42;
switch(ch) {
case 3.14: //错误,不是整数
case ival://错误,不是常量
}
任何两个case标签的值不能相同,否则引发错误。
一般不要省略case分支最后的break语句,如果没有写break语句,最好加一段注释说清楚程序的逻辑。
do while
do
statement
while(condition);
对于do while
来说先执行语句或者快,后判断条件,所以不允许在条件部分定义变量,因为变量必须先定义才能使用,如果在statement中使用,再在condition中定义的话是错误的。
break
负责终止离它最近的while,do while,for,switch
语句continue
只能出现在for,while ,do while
中,只有当switch
语句嵌套在迭代语句内部时,才能在switch
里使用continue
- 异常处理机制为程序中异常检测和异常处理两部分协作提供支持,在C++中,异常处理包括:
- throw 表达式 异常检测部分使用
throw
表达式来表示它遇到了无法处理的问题。我们说trow
引发了异常 - try语句块 异常处理部分使用
try
语句块处理异常,语句块以 关键字try
开始,并以一个或多个catch子句结束,try
语句块中代码抛出的异常通常会被某个catch子句
处理,因为catch子句处理异常,所以它们也被称作异常处理代码。
异常讲解
5.25习题
1 #include<iostream>
2 using namespace std;
3 int main()
4 {
5 int item1, item2;
6 while(cin >> item1 >>item2) {
7 try {
8 if(item1 != item2) {
9 throw "两个数不相等";
10 }
11 } catch (const char *s) {
12 cout << s << "\nTry again?Enter y or n " << endl;
13 char c;
14 cin >>c;
15 if (!cin || c == 'n') {
16 break;
17 }
18 }
19 }
20 cout << "异常之后后面语句照样执行" << endl;
21 return 0;
22 }
函数
- 函数形参和实参的区别
形参是函数定义中声明的参数,用于接收函数调用时传递的值或表达式,他们在函数体内部使用。实参是在函数调用时传递给函数的值或表达式,用于填充函数定义中的形参,它们在函数调用语句中使用。正确使用形参和实参可以帮助我们编写更加清晰,简洁和刻度的函数调用语句,提高代码的可维护性和可重用性。 - 静态局部变量 的生命周期 = 程序的生命周期,且只会初始化一次,储存位置是程序的静态储存区域,不是在堆栈中,静态储存区还有全局变量和常量,它的大小和位置在程序编译时就已经确定了。但是并不能保证静态局部变量一定存储在静态区中,可能因为编译器和平台而有所变化。
- 函数的声明应该放在头文件中
如果将声明放在源文件中的话会出现如下问题
- 多个源文件中声明可能不一致,如函数参数个数,类型和返回值等不同
- 函数重复定义,可能会导致链接问题。
- 函数如果无需改变引用传参的值,最好将其声明为常量引用
bool add(const string &s1, const string &s2);
- 当使用实参初始化形参时会忽略掉顶层const
egvoid func(const int i);
这个函数无论是int 还是const int都能传参 - 尽量把函数不会改变的形参定义成常量引用,传参的权限只能缩小,不能放大,形参定义成常量引用,既能传参数为变量的实参,也能传参数为常量的实参。
- 数组传参是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,此时在我已知传参的方式中还有一种额外方式,使用标准库中的begin和end函数
int arr[10] = {0};
print(begin(arr), end(arr));
- 数组也可以引用传参
void print(int (&arr)[10]);
但是有限制,此时只能传递有十个整数的数组 - 当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
含有可变形参的函数
,如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。initializer_list类型定义在同名的头文件中,它提供的操作如下:
initializer_list<T> lst;
默认初始化; T类型元素的空列表
initializer_list<T> lst{a, b, c...};
lst的元素类型和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst)或lst2 = lst;
拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后原始列表和副本共享元素
lst.size() 列表中元素的数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中尾元素下一位置的指针
和vector一样,initializer_list 也是一种模版类型,因此定义initializer_list对象时,必须说明列表中所含元素的类型。
eg
initializer_list的元素类型是string
initializer_list<string> ls;
initializer_list的元素类型是int
initializer_list<int> li;
如果想向initializer_list
中传递一个值的序列,则必须把序列放在一对花括号内:
3 void error_msg(initializer_list<string> il)
4 {
5 for(auto beg = il.begin(); beg != il.end(); ++beg) {
6 cout<< *beg << " ";
7 cout << endl;
8 }
9 }
10 int main()
11 {
12 int a;
13 cin >> a;
14 if(a != 1) {
15 error_msg({"function", "hello", "world"});
16 }
含有initializer_list
形参的函数也可以同时拥有其他形参,例如如果上述函数的第一个参数是int,第二个参数是initializer_list,则传入时可以在原来的基础前填一个int整数
注:在范围for中循环使用initializer_list对象时,应该将循环控制变量声明成引用类型,并且引用的类型应该是const &
因为initializer_list对象的元素是const类型不可修改,并且如果不用引用的话,会增加额外的拷贝。
- 不要返回局部对象的引用或指针,函数一旦完成后,局部对象将被释放掉,一旦释放掉以后指针或引用将指向一个不存在的对象。要想确保返回值安全,必须确保在函数调用完成之后该对象依旧存在,或者说该对象指向的空间依然存在
- C++11规定,函数可以返回花括号包围的值的列表。
eg:
11 vector<string> process()
12 {
13 //..
14 //expected 和 actual 是string的对象
15 string expected, actual;
16 if(expected.empty()) {
17 return {}; //返回一个空的vector对象
18 } else if (expected == actual) {
19 return {"functionX", "okey"}; // 返回列表初始化的vector对象
20 } else {
21 return {"functionX",expected, actual};
22 }
23 }
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,如果返回的是类类型,由类本身定义初始值如何使用,也就是类类用返回值初始化。
- 声明一个返回数组只指针的函数有时候返回值会显得十分复杂
eg
int (*func(int i))[10]
该函数的名字是func
,形参类型是int
返回值是指向含有10个整形数组的指针
在c++11中有一种可以简化上述func
声明的方法,就是使用尾置返回类型。
为了表示真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方防止一个auto
auto func(int i) -> int(*)[10];
函数重载
在同一个作用域内的几个函数名字相同但是形参列表不同。- 一个拥有顶层const的形参无法和另一个没有顶层的形参区分开来,他们二者不能实现函数重载,而拥有底层const的形参与非常量对象可以实现函数重载。
25 Record lookup(Phone);
26 Record lookup(const Phone);
27 //重复声明了Record lookup(Phone)
28
29 Record lookup(Account&); //函数作用于Account的引用
30 Record lookup(const Account&);//函数作用于常量引用
前面两个函数不能构成重载,此时调用函数无法区分开
后面两个可以,因为常量只能调用常量版本的,虽然非常量对象可以转化成const对象进而调用const版本,但当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
const_cast 和重载
const_cast只能改变运算对象的底层const,其在重载函数的情景中最有用
举个例子:
5 //比较两个string对象的长度,返回较短的那个引用
6 const string &shorterString(const string &s1, const string &s2)
7 {
8 return s1.size() <= s2.size() ? s1 : s2;
9 }
10 /*
11 * 该函数的参数和返回类型都是const string 的引用
12 * 我们可以对两个非常量的string实参调用这个函数
13 * 但返回的结果仍然是const string&
14 */
15
16 //运用const_cast达到传入的实参不是常量,但是中间调用上述函数
17 //返回值不是常量
18 string &shorterString(string &s1, string &s2)
19 {
20 auto &r = shorterString(const_cast<const string&>(s1),
21 const_cast<const string&>(s2));
22 return const_cast<string&>(r);
23 }
24 /*
25 * 这个函数,首先将实参强制转化成const的引用,然后调用
26 * shorterString函数的const版本,const版本返回对const string&的引用
27 * 然后我们再将其转换回一个普通的string&
- 当调用重载函数会出现如下三种情况
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也将发生错误,称为二义性调用
- 函数重载必须在同一个作用域。
- 在c++语言中,名字查找发生在类型检查之前。
- 上述中我们编写了一个小函数,作用是比较两个
string
形参的长度并返回长度较小的string
引用,把这种规模较小的操作定义成函数可以提高代码的复用率,并且修改操作变得更加容易,但将操作定义成函数存在一个潜在的缺点,函数调用比一般求等价表达式的值要慢一点,在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行 内联函数
可避免函数调用的开销
将函数指定为内联函数,通常就是将它在每个调用点上"内联的“展开”。
在函数的返回类型前面加上关键字inline,就可以将它声明成内联函数了
eginline const string shorterStirng(const string &s1, const string &s2);
内联说明只是向编译器发出一个请求,编译器可以忽略这个请求,一般而言,内联机制用于优化规模较小,流程直接,频繁调用的函数。constexpr函数
是指能用于常量表达式的函数,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数,但是是允许constexpr
函数的返回值并非一个常量
eg:
7 constexpr int new_sz() { return 42; }
8 constexpr int foo = new_sz(); // 正确,fool是一个常量表达式
9 //如果arg是常量表达式,则scale(arg) 也是常量表达式
10 constexpr size_t scale(size_t cnt) { return new_sz() * cnt;}
11 int arr[scale(2)];//正确,scale(2)是常量表达式
12 int i = 2; // i不是常量
E> 13 int a2[scale(i)]; //错误:scale不是常量表达式
assert
是一种预处理宏,执行运行时检查,其行为依赖预一个名为NDEBUG
的预处理变量的状态,即如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG,则assert将执行运行时检查。同时,很多编译器都提供了一个命令行选项是我们可以定义预处理变量。
g++ -D NDEBUG test.cc -o test
加上-D NDUBUG就可以函数匹配
在大多数情况下,我们容易确定某次调用应该选用哪个重载函数,但是如果几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时,函数调用就不怎么容易了。
以下面的函数为例
7 void f();
8 void f(int);
9 void f(int, int);
10 void f(double, double = 3.14);
如果出现f(5.6)
问,此时会调用哪个函数
- 先选与被调用函数的同名函数
在这个例子中,四个函数都是同名函数 - 形参梳理与本次调用提供的实参数量相等
- 每个实参的类型与对应的形参类型相等,或者能转换成形参的类型
因此上述四个函数中
我们首先能根据实参的数量排除两个,剩下的使用一个int实参和两个double(其中一个有缺省量)是可以的。
f(int)
是可行的,double可以转化成int
f(double, double = 3.14)
也是可以的,因为第二个参数类型完全一致,都是double,所以此时匹配是f(double, double = 3.14;
基本思想:实参类型与形参类型越接近,他们匹配的越好
如果是调用f((42,3.14)
,c此时根据第一条规则,在f(int, int) 与f(double, double = 3.14)但是发现如果只看第一个参数的话f(int, int)适合,只看第二个参数的话f(double, double = 3.14适合,编译器最终将这个调用具有二义性而拒绝请求。
注:调用重载函数时,应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
函数指针
指向函数,和其他指针一样,函数指针指向特定类型,函数指针的类型由它的返回类型和形参类型共同决定,与函数名无关。
eg
bool (*pf) (const string&, const string&);
pf先与*
结合说明其是一个指针,将名字拿掉此时剩下的就是指针指向的类型了,该类型是形参为两个cosnt string&以及返回值是bool的函数。
当我们把函数名作为一个值使用时,该函数自动转换成指针。
我们还能直接使用函数的指针调用该函数。
在指向不同函数的指针间不存在类型转换规则。- 函数指针形参
11 void useBigger(const string &s1, const string &s2,
12 bool (*pf)(const string&, const string&));
若把函数作为实参使用,此时它会自动转换成指针
eg
useBigger(s1, s2, lengthCompare);
如上所示,直接使用函数指针显得冗长而繁琐,使用typedef
和decltype
可以简化使用了函数的代码
13 //Func 和 Func2是函数类型
14 typedef bool Func(const string&, const string&);
15 typedef decltype(lengthCompare) Func2;
16 //FuncP和FuncP2是指向函数的指针
17 typedef bool(* Func)(const string&, const string&);
18 typedef decltype(lengthCompare)(*Func2);
需要注意的是decltype返回的是函数类型,并不是函数指针,此时我们要自己加上*
才表示函数指针。
- 返回指向函数的指针
eg
int (*f1(int))(int, int);
该函数名为f1,参数为一个int,返回指向函数形参为两个int,且返回值为int函数的指针。