1 基础
1.1 范围和生命期
- 一个名字的范围是指该名字在程序中的作用域,即可见范围。
- 一个对象的生命期是指在程序执行时,对象存在的持续时间。
一个
全局对象的生命期从程序创建时开始,到程序终止时结束。
局部对象的生命期开始于其定义的位置,当程序控制路径越过其所在的作用范围时,生命期结束。加上
static声明的局部对象,其初始化发生在程序第一次执行到该对象的定义之前。其生命期在函数调用结束后仍然持续。
void go()
{
static int i = 1;
++i;
}
int main() {
go();
go();
return 0;
}
当程序执行到第2行 '{' 时,变量i初始化为1,第一个go()调用后i为2,进入第二个go()调用时,i = 2。
1.2 函数声明
函数的声明不带函数体,即将大括号换成分号:
void fun(int, int);
在函数声明中参数的名字是不必要的。函数声明包括三部分:
返回值,
函数名,
参数类型。这三部分描述了函数接口。函数声明也称为
函数原型。
一般将函数的声明放在头文件中,定义放在源文件中。源文件需要包含有函数声明的头文件。
2 参数传递
函数调用时的参数传递有两种方式:1. 传值; 2. 传引用。对于占用内存很大的对象,传递引用可以避免大量的空间开销。
2.1 尽可能使用const引用
使用const引用或字面值初始化一个普通的引用是错误的,该规则对于参数的传递也适用。
void go(int &i)
{
}
int main() {
int i = 1;
const int &ri = i;
go(ri); // error
go(10); // error
return 0;
}
加上const
void go(const int &i)
{
}
int main() {
int i = 1;
const int &ri = i;
go(ri); // ok
go(10); // ok
return 0;
}
2.2 传递数组
由于数组的特殊性,在使用数组时,会自动转换成指针。即:
int a[10];
int *p = a;
cout << a[5] << endl; // 等价于 *(p+5)
在传递数组时,实际上传递的是指向数组的指针。
void go(int *pa);
void go(int a[]); // 强调传递的是数组
void go(int a[10]); //
...
int a0 = 0;
int a1[2] = {0,1};
int a2[15] = {0,1};
go(&a0); // ok
go(a1); // ok
go(a2); // ok
这三种定义方法在某种程度上是等价的,因为在调用go()时,编译器只会检查实参的类型是否是int,即检查数组元素的类型。所以上面的调用都是合法的,但是数组范围的可控制则需要很小心。看下面的示例:
void go(int a[10])
{
for (int i=0; i<10; ++i)
cout << a[i] << endl;
}
int main() {
int a1[2] = {0,1};
int a2[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
go(a1);
go(a2);
return 0;
}
go(a1)调用之后,输出的十个数,后面八个是未定义的,因为数组a1只定义了两个元素;
go(a2)调用后,输出数组a2的前10个元素。如果将函数稍作修改,让其输出15个元素,这也未尝不可
void go(int a[10])
{
for (int i=0; i<15; ++i)
cout << a[i] << endl;
}
go(a2)调用后,输出a2的15个元素。你可能会以为后五个元素是未定义的,因为形参是
int a[10]。但是,前面已经指出,它和
int *pa的定义方法等价,因为数组就是指针。
2.2.1 传递数组的时候如何获取数组的长度信息?
- 对于C-风格字符串,我们可以利用末尾的NULL符号判断;
- 使用标准库的风格,指定数组的开始位置和结束位置;
我们知道end指向的是数组的超出末端位置,不能对其引用。void go(int *beg, int *end) { while (beg != end) { cout << *beg++ << endl; } }
利用新规则,我们可以很方便的得到这两个指针
int a[10] = {}; go(begin(a), end(a));
- 将数组长度作为形参传递。
2.2.2 数组的引用
可以将形参定义为数组的引用:
void go(int (&ra)[10]); //(&ra)的括号不能省
因为数组的大小是其类型的一部分,所以调用go时传递的实参也必须严格的是int [10],显然这会限制其使用。
2.2.3 传递多维数组
严格的讲,C++中没有多维数组。所谓的多维数组实际上是指
数组的数组。定义方法如下:
void goMul(int (*paa)[5], int crow)
{
for (int row=0; row < crow; ++row) {
for (int col=0; col < 5; ++col)
cout << paa[row][col] << " ";
cout << endl;
}
}
int main() {
int aa[2][5] = {
1, 2, 3, 4, 5,
6, 7, 8, 9, 10
};
goMul(aa, 2);
return 0;
}
2.2.4 main函数参数
int main(int argc, char **argv); 或者 int main(int argc, char *argv[])。
argv 是保存char*类型的指针数组,即保存的C-风格字符串。argc表示字符串数。系统将命令行参数传递给main,argv中的第一个字符串是程序名,或者为空串。可选参数从argv[1]开始。
2.3 可变参数
有时候我们事先不知道要传递多少个参数,像printf库函数就是一个例子。新标准有两种方法实现可变参数方程。
2.3.1 initializer_list 参数
如果所有的参数类型相同,我们可以使用initializer_list模板。initializer_list<T> lst; | 默认初始化,元素类型为T的空列表 |
initializer_list<T> lst{a,b,c...}; | lst的元素个数可以和initializers一样多;元素是对于的 initializers的副本。列表中的元素是const的 |
lst2(lst) lst2 = lst | 对initializer_list拷贝或赋值不会复制列表中的元素。复制后 副本和原列表共享元素 |
lst.size() | 列表中的元素数 |
lst.begin() lst.end() | 返回lst中第一个元素和过一个最后元素的指针。 |
2.3.2 省略号参数
来源自C语言库中的
varargs.printf是一个很好的例子:
int printf(const char* _Format, ...);
3.函数返回值
返回值为非void的函数必须有返回值。使用
return val;语句返回一个合适的值。
当函数返回一个值的时候,是将返回值赋给一个临时变量,该变量就作为函数调用的结果。
[注意]返回局部对象的引用或指针会导致严重的错误:因为当函数调用结束后,局部对象将被释放,引用为定义的对象是危险的。
如果函数的返回一个引用类型,则该函数调用可以作为
左值;否则为右值。
int &swapXY(int &x, int &y)
{
int temp;
temp = x;
x = y;
y = temp;
return x;
}
int main() {
int x = 10, y = 5;
swapXY(x,y) = 0;
return 0;
}
返回列表
返回列表类似于列表初始化的规则,使用列表来填充临时返回变量。
string ss(bool state)
{
if (state)
return {"Ok"};
else
return {"Error", "Exit"};
}
返回指向数组的指针
- 使用数组别名
typedef int arr[10]; using arr = int[10]; arr* func(int i);
- 直接定义
Type ( *function(parameter_list) ) [dimension]
int (*func(int i)) [10];
- 使用auto
auto func(int i) -> int(*)[10];
- 使用decltype
int arr[10] = {1,2,3}; decltype(arr) *func(int i) { }
4 重载
对于一个函数,与其函数名相同,而参数类型或参数个数不同的函数是其重载函数。
如果判断两个函数是否相同是关键,下面列举的都是相同的函数声明:
int overLoad(int a); // 原函数
int overLoad(int); // 变量名可以忽略
typedef int INT;
int overLoad(INT a); // int 和 INT是相同的类型
float overLoad(int a); // error: 只有返回值不同
int overLoad(const int); // top-level const对于可以传递的实参的类型没有影响
4.1 const_cast与重载
有时候我们可能同时需要一个函数的const版本和非const版本,这时const_cast就很有用了。
const int &go(const int &a)
{
return a;
}
int &go(int &a)
{
return const_cast<int&>(go(const_cast<const int&>(a)));
}
非const版本的实现调用了const版本,这是一种不错的重载函数实现方法。因为
同名的重载函数一般都完成相同的任务,利用其中一个去实现其他的,这样在函数需要修改时,就很方便了。
当定义好重载函数以后,需要考虑的就是函数匹配问题。即编译器需要根据实参的类型去匹配应该调用哪个函数。一般可以根据参数个数和类型决定。但是当一些参数可以通过类型转换而与一些函数发生联系时,就不容易匹配了。
4.2 专用特性
4.2.1 默认参数
如果某个参数在大多数情况下的值是固定的,而又希望可以另行指定值。我们可以为参数提供默认的初始化值。
void go(int, int = 10, int = 5);
void go(int i0, int i1 = 10, int i2 = 5)
{
}
如果一个参数具有默认值,那么其后的所以参数也需要提供默认值。这种规定可以使我们在调用函数时,
全部或部分省略有默认值的参数。
go(10);
go(10,20);
go(10,20,30);
【技巧】在设计函数时,将最不常使用默认值得参数放在前面,最常使用默认值的放在后面。
4.2.2 inline函数
inline机制用来优化一些小的、很直接的且调用频繁的函数。inline函数在编译时将会被展开,避免了函数调用带来的额外开销。
比较x,y,z的大小,返回最大值:
inline int maxXYZ(int x, int y, int z)
{
return x > y ?
(x > z ? x : z):
(y > z ? y : z);
}
4.2.3 constexpr函数【C++11】
constexpr函数是可以在 常量表达式中使用的函数。constexpr函数与其他函数相比有一些 限制: 返回值类型和每个参数的类型必须是字面值(literal type),函数体中只能包含一个return语句。
constexpr函数默认是内联函数。constexpr函数的函数体可以包含一些在运行时不产生动作的语句,比如:空语句,类型别名,using声明等。constexpr函数可以返回一个非常量的值,但是调用函数的实参必须是常量表达式。
【注】inline函数和constexpr函数的定义一般都放在头文件中。
4.3 调试助手
assert是一个预处理宏。定义:
#define assert(_Expression) (void)( (!!(_Expression))
|| (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )
该宏用来测试一个表达式的值是否为真。如果为假,则会中断程序的执行,并提示出错的地方。
assert一般用于程序的调试,它与NDEBUG标示符相关联,如果该标示符定义了,则assert会失效,即不进行检查。所以assert不能替代程序运行时的逻辑检查或错误检查。
C++预处理器定义了几个有用的变量方便调试:
- __FILE__ string literal containing the name of the file
- __LINE__ integer literal containing the current line number
- __TIME__ string literal containing the time the file was compiled
- __DATA__ string literal containing the data the file was compiled
5 函数匹配
先看一个例子:
void go();
void go(int);
void go(int, int);
void go(double, double = 1.0);
...
go(3.14);
函数调用会和
go(double, double = 1.0);匹配。
第一步是确认
候选函数。候选函数是指当前可见的(在之前声明过)与调用函数同名的函数。
第二步是从候选函数中选取
可行函数。参数个数必须相同,实参类型必须和形参类型一致或者可以类型转换为一致。在上面的例子中,可以排除没有参数,和有两个int形参的候选函数。有两个double形参的函数因为含有默认初始化,所以可以使用单独一个参数调用。
第三步是从可行函数中选取
最佳匹配函数。衡量匹配度的标准是实参类型与形参类型接近程度。5.6当然与double类型更接近,不需要类型转换,所以上面的例子中应该与go(double, double = 1.0);匹配。
如果有多于一个的参数,则匹配会更复杂。此时选取的最佳匹配函数应该满足:
- 每个参数的匹配情况都不必其他函数差;
- 至少一个参数的匹配比其可行他函数更佳。
如果没有函数满足上面两点,则会产生歧义,即调用出错。比如:go(5, 5,1);就是一个错误的调用。
为了确定最佳匹配,编译器对类型转换进行了排名:
- 准确匹配。包括:形参类型和实参类型相同;实参是从数组或函数类型转换成对应的指针类型;在实参中加入或丢掉顶级const。
- 通过const转换匹配
- 通过类型提升匹配
- 通过算术或指针转换匹配
- 通过类类型转换匹配
6 指向函数的指针
一个函数的类型由它的返回值和参数决定,函数名不是类型的一部分。所以我们可以定义指向函数的指针:
bool cmp(const void *a, const void *b);
bool (*pf)(const void *a, const void *b) = cmp;
函数指针必须赋值为与其类型相同的函数,不能指向不同类型的函数。当然,函数指针可以指向重载函数,可以作为函数的参数或返回值。由于函数指针的声明比较复杂,我们可以利用新标准的decltype和auto关键字来简化。
typedef decltype(cmp) FUNC_TYPE; //
函数指针作为函数返回值:
FUNC_TYPE *getCmpFunc(); // decltype返回的是函数类型而不是函数指针
using PFUNC_TYPE = bool (*)(const void *, const void *);
PFUNC_TYPE getCmpFunc();
auto getCmpFunc() -> bool (*)(const void *, const void *);