0.一些有用的概念
<0>: 自动对象:对于普通局部变量对应的对象而言,当函数的控制路径经过该变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象。
<1>:局部静态对象:在程序的执行路径第一次经过对象的定义语句时初始化,并且知道程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响!例如:
SizeType count_calls() {
static SizeType ctr = 0;
return ++ctr;
}
int main () {
for (SizeType i = 0; i != 10; ++i) {
cout << count_calls() << endl;
}
}
<2>:头文件中进行函数声明:把函数声明直接放置在函数的源文件中是合法的,但是繁琐且容易出错。相反的,将函数声明放入头文件中,就能够保持同一函数的所有声明是一致的,并且在我们想要改变函数的接口时,只需要改变一条声明。
<3>:分离式编译:允许我们把程序分割成几个文件,每个文件独立编译。先分离式编译,然后链接成一个可执行文件。
1.传递参数
<0>:指针形参:指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值,而不是指针所指向的值。拷贝之后,两个指针是不同的指针。我们可以通过指针间接的访问它所指向的对象,因此我们也可以通过指针简洁的修改它所指向对象的值。
void reset(int *p) {
*p = 0; // 这个是将p所指向的对象的值改为了0
p = NULL; // 这个是将指针变量的值改为了NULL,但所指向对象的值不变
}
<1>:引用参数:对于引用变量的操作,就相当于对实际引用对象的操作。
int reset(int &i) { // i是传递给reset函数的另一个名字
i = 0; // 将实际的引用对象的值改为了0
}
PS<1>:使用引用避免拷贝:拷贝大的类类型或者容器对象比较低效,甚至有些类类型不支持拷贝。当不支持拷贝时函数就只能通过引用形参来访问该类型的对象。
当然在使用引用时,我们也要注意程序本身对于所引用对象的一些操作,如果我们不希望再经过函数片段后更改了所引用的对象,我们就要谨慎的使用引用!
PS<2>: 使用引用类型返回额外的信息:函数只能一次返回一个值,但是我们经常会想要函数能狗尽量的返回多个值,提供给我们更多的信息,此时引用类型为我们提供了返回多个值的有效途径。
我们仍然只让函数返回了一个值,但是我们将想要获得的值的变量定义再函数引用的前面,通过传入该对象的引用,再函数内部对其进行操作,也就间接的获得了多余的信息。例如:
// 我们想要获得某个字符串中特定字符的第一次出现的位置索引和出现的总次数
int find_char(string &s, char c, int &occurs) { // 字符串引用变量,次数引用变量
int res = -1;
occurs = 0;
for (int i = 0; i <= s.size() - 1; ++i) {
if (s[i] == c) {
if (res == -1) {
res = i;
}
++occurs;
}
}
return res;
}
<2>:const 形参和实参
0.顶层const:表示变量本身就是一个常量;底层const:表示所指向的或者引用的变量是一个常量
int i = 0;
int *const p1 = &i; // 有*号,p1表示指针,const跟在p1前面,表示p1是一个常量,也就是p1是一个指针常量,是一个顶层const
const int ci = 42; // 无*号,单纯表示ci是一个常量,顶层const,这类变量只能用底层const指针变量来引用
const int *p2 = &ci;// 有*号,但不在p2前面,表示p2是可以改变的,不是一个常量,但p2所指向的对象是一个常量
const int *const p3 = p2; // 靠右的const是顶层const,靠左的是底层const
const int &r = ci; // 用于声明引用的const都是底层const
i = ci; // i是一个普通变量,没有问题
p2 = p3; // 正确,p2是一个底层const修饰的指针变量,指向对象是一个常量,p3既是一个顶层const也是底层const修饰的指针变量
int *p = p3; // 错误:p3包含一个底层const,表示指向的对象是一个常量,而指针类的变量需要是底层const修饰的才能够去指向一个常量
p2 = p3; // 正确:p2是一个底层const修饰的,它可以指向一个常量
p2 = &i; // 正确:int * 可以转换成const int *
int &r1 = ci; // 错误:常量不能被绑定再普通的引用变量上
const int &r2 = i; // 正确:普通的变量可以绑定到const int &引用变量上!
注意:使用const修饰词时,一定要谨慎使用,不要造成理解上的错乱。
1.函数参数中的const
<0>:形参中的顶层const被忽略掉了:和其他的初始化过程一样,使用实参去初始化带有const的形参时,const会被自动的忽略掉。例如:
void func(const int i) { } // 这个和下面一个函数本质是一样的
void func(int i) { } // 这两者在编译过程中会出现同一个函数声明两次的错误!
<1>:指针或引用形参与const
我们知道,形参的初始化和变量是一样的。我们可以用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i; // 正确,但是cp不能修改i的值
const int &r = i; // 正确,但是r也不能修改i的值
const int &r2 = 42; // 正确:
int *p = cp; // 错误:p是普通指针类型,可以修改值,但是cp是底层const类型,矛盾
int &r3 = r; // 错误:r3是普通类型的引用变量,可以修改值,但r是const类型,矛盾
int &r4 = 42; // 错误
将同样的初始化规则用到参数传递上有:
void reset(int *p) {
*p = 0; // 这个是将p所指向的对象的值改为了0
p = NULL; // 这个是将指针变量的值改为了NULL,但所指向对象的值不变
}
int reset(int &i) { // i是传递给reset函数的另一个名字
i = 0; // 将实际的引用对象的值改为了0
}
// 我们想要获得某个字符串中特定字符的第一次出现的位置索引和出现的总次数
int find_char(const string &s, char c, string::size_type &occurs) { // 字符串引用变量,次数引用变量
int res = -1;
occurs = 0;
for (int i = 0; i <= s.size() - 1; ++i) {
if (s[i] == c) {
if (res == -1) {
res = i;
}
++occurs;
}
}
return res;
}
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); // 正确
reset(&ci); // 错误:ci是顶层const类型,不能修改值
reset(i); // 正确,调用后一个reset函数
reset(42); // 错误,字面常量
reset(ctr); // 错误:类型不匹配
find_char("Hello World", 'o', ctr); // 正确
<2>:尽量使用常量引用
把不会改变的形参定义成(普通)的引用是一种很常见的错误,这样做会给函数的调用者带来很大的一种误解,认为函数可以修改它的实参的值。此外,使用引用而不是常量引用会在很大的程度上限制函数所能够接受的参数。比如,当你声明一个函数,它的参数是一个普通的引用参数,那么如果我想要传入一个常量值,那么就会造成编译错误。而我如果声明为一个常量引用,我传入一个正常的参数值,那么它是可行的。例如:
void print(int &i) {
cout << i << endl;
return;
}
如果我在这个函数中传入2,即print(2);那么将会出现错误。但如果我们声明如下:
void print(const int &i) {
cout << i << endl;
return;
}
此时我使用print(2);不会出现错误,同时使用print(j);(j=33)也不会出现错误,都是合法的。
2.数组形参----重点、重点、重点
数组有两个诡异的特殊性质:
i:不允许拷贝数组 ii:使用数组时通常会转换成指针
<0>:虽然不能拷贝数组,但是我们在给函数名命形参时,仍然可以用数组的形式表示
// 尽管形式不同,但三个函数是等价的
// 每个函数都有一个const int *类型的形参
void print(const int *);
void print(const int []);
void print(const int [10]); // 这个10表示我们所期待的数组的大小,但实际上编译会忽略掉
PS:因为数组是以指针的形式传递给函数,所以函数一开始并不知道数组的具体大小,调用者应该为此提供一些可用的信息。
<1>:管理指针形参的三种常用方法
i:使用标记指定数组的长度:其实这种方法,函数本身也并不清楚数组的大小,但是我们能够通过数组内本身存在的一个标记来指定数组的结束位置。比如:
void print(const char *p) { // p是一个字符串的指针,通过特殊标记来识别数组的大小
if (p != NULL) {
while (*p != '\0') {
cout << *p << endl;
}
}
}
ii:使用标准库规范:通过向函数中传递两个指针,一个数组的首元素指针,一个数组的尾后元素指针,例如:
void print(const int *beg, const int *end) {
while (beg != end) {
cout << *(beg++) << endl;
}
}
iii:显式的传递一个表示数组大小的形参,例如:
void print(const int ia[], int size) {
for (int i = 0; i <= size - 1; ++i) {
cout << ia[i] << endl;
}
}
<2>:数组形参和const
当函数不需要对数组元素进行写操作时,数组形参就应该定义成指向const的指针,只有当函数确定需要对数组元素进行改写操作时,才把数组形参定义成指向非常量的指针。
<3>:数组的引用形参
C++允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。例如:
void print(int (&arr)[10]) {
for (auto elem : arr) {
cout << elem << endl;
}
}
注意:&arra两端的括号必不可少
f(int &arr[10]) // 错误,将arr声明成了引用的数组
f(int (&arr) [10]) // 正确:arr是一个引用,是一个含有10个int类型元素的数组的引用
同时,数组的引用形参也会带来一些不便,数组的形参它是某一个具体大小的数组的新参,也就是说,他会在声明的时候就确定了能够传进函数的数组的大小,这在某种程度上会带来不便。例如:
int i = 0, j[2] = {0, 1};
int k[10] = {1,2,3,4,5,6,7,8,9,10};
print(&i); // 错误:参数不匹配
print(j); // 错误,数组大小不匹配
print(k); // 正确
<4>:传递多维数组
我们知道,其实c++并没有所谓的多维数组,而我们所理解的多维数组,实际上是一维数组的数组。所以说,当我们要传递多为数组给函数时,实际上是要传递指向数组的首元素的指针。因为我们在处理数组的数组时,第一个数组的元素本身就是一个数组,也就是下一个数组的首元素的地址,指针就是指向一个数组的指针。例如:
void print(int (*Matrix)[10], int rowSize) {}//
// 等价定义
void print(int Matrix[][10], int rowSize);
注意:int *Matrix[10]; // 表示的是一个含有10个指针的数组
int (*Matrix)[10]; // 表示Matrix是一个指针,指向含有10个int类型元素的数组
<5>: 含有可变形参的函数
i:如果我们已知函数的实参数量是未知的,但是类型是一样的,我们可以使用initializer_list类型的形参。initializer_list提供的一些操作:
intializer_list<T> lst; // 默认初始化,T类型元素的空列表
initializer_list<T> lst{a,b,c...} // lst的元素和初始值一样,是对应初始值的副本,列表中元素是const
lst2(lst) // 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素
lst2 = lst // 拷贝后,原始列表和副本共享元素
lst.size() // 返回列表中元素的数量
lst.begin() // 返回指向lst首元素的指针
lst.end() // 返回指向lst尾元素下一个位置的指针
例如:
void erro_msg(initializer_list<string> il) {
for (auto beg = il.begin(); beg != il.end(); ++beg) {
cout << *beg << " ";
}
}
ii:省略符形参
省略符形参是为了便于C++程序访问一些特殊的C代码所设置的,这些代码使用了名为varargs的C标准库功能。
省略符形参仅仅用于C和C++通用类型,特别应该注意,大多数类型的对象在传递给省略符形参时都无法正常拷贝
省略符形参只能放在形参列表的最后一个位置,它的形式有两种:
void foo(para_list, ...); // 这里的...是可以省略的,即void foo(para_list, )
void foo(...);
2.返回类型和return语句
<0>:返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值是用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
例如函数:
string make_plural(size_t ctr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
该函数返回类型是string,意味着返回值将被拷贝到调用点。因此,该函数将返回word的副本或者一个未命名的临时string对象,该对象内容是word和ending的和。
<1>:不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉了,因此,函数终止意味着局部变量的引用将指向不在有效的内存区域。
<2>:引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值一样来使用返回引用的函数的调用。特别是,我们还能够为返回非常量引用的函数的结果赋值。例如:
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}
int main ()
{
string s{"a value"};
cout << s << endl;
get_val(s, 0) = 'A'; // 如果返回常量引用就不可以这么使用
cout << s << endl;
return 0;
}
// 这是一种非常神奇的操作!!!
<3>:列表初始化返回值
C++11规定,函数可以返回花括号包围的值的列表。此列表用于对表示函数返回值的临时量初始化。例如:
vector process()
{
if (expect.empty()) {
return {};
} else if (expect == actual) {
return {"FunctionX", "ok"};
} else {
return {"FunctionX", expect, actual};
}
}
如果返回类型是内置类型,列表则最多包含一个值。
<4>:返回数组指针
<0>:数组不能拷贝,所以不能直接返回数组。但是我们可以返回数组的指针或者引用。定义一个返回数组的指针或者引用的函数比较的麻烦,但是我们可以通过一些方法简化,例如:
typedef int arraT[10]; // arraT是一个类型别名,表示含有10个int整数的数组
using arraT = int[10]; // 与上面等价,这是C++有的C没有的用法
arraT* func(int i); // 返回一个指向10个整数的数组指针。
int arr[10]; // 含有10个整数的数组
int *p[10]; // 含有10个指针的数组
int (*p)[10]; // 指向10个整数组数的指针
<1>:声明一个返回数组指针的函数(不使用类型别名)
Type (*function_name(parameter_list)) [dimension];
// 理解方法:从里往外
// function_name(parameter_list) :调用该函数时需要传入啥参数
// (*function_name(parameter_list)):表明我们可以对函数的调用执行解引用操作,即返回值是一个指针
// (*function_name(parameter_list))[dimension]:dimension个元素的数组的指针
<2>:使用尾置返回类型
C++11有可以简化上述函数声明的方法。尾置返回类型是在形参列表后面用一个->符号开头。为了表示函数真正返回的类型跟在形参列表之后,我们在本该出现返回类型的地方放置了一个auto:
auto function_name(parameter_list) -> Type (*)[dimension];
<3>:使用decltype
当我们知道返回的指针将指向哪一个数组时,我们就可以使用decltype关键子来声明返回类型了。例如:
int odd[] = {1,3,5,7,9};
int even[] = {2,4,6,8,10};
decltype(odd) * arrPtr(int i) // 返回的是指向数组的指针
{
return (i % 2) ? &odd : &even; // odd本身是指向自己的指针
}
// 理解:decltype(odd):表示数组,加一个*,表示指向odd这类的数组的指针
所以使用方式:
decltype(odd) *arrp = arra(1);
for (int i = 0; i <= 4; ++i) {
cout << (*p)[i] << endl;
} // (*p):表示是数组了,然后再取数组里的值
3.函数的重载(overloading)
定义:同一作用域内函数名相同,但是形参列表不同的几个函数称为重载函数。
PS:我们不允许两个函数,出了返回类型不同其他都相同的情况,编译器会报错。
<0>:重载和const形参
顶层const不影响传入函数的参数。一个拥有顶层const的形参无法与没有顶层const的形参区分开来:
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了lookup函数
Record count(Phone *);
Record count(Phone *const); // 重复声明了count函数
<1>:const_cast和重载
const_cast在重载函数中最有用!!!E.g:
const string &shorterString(const string &s1, const string &r2)
{
return s1.size() < s2.size() ? s1 : s2;
}
我们对两个非常量的string参数调用该函数,返回值是const string的引用。我们需要一种新的shorter string函数,当它的实参不是常量时,得到的结果是一个普通引用,const_cast可以实现,例如:
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&> (s1), const_cast<const string&> (s2));
return const_cast<string&> (r);
}
这个版本中,我们先对s1,s2进行强制转换---const_cast<const string&>; 调用上一个版本的shorterString;返回了一个const string的引用给r,这个引用实际上是绑定在一个储实的非常量的实参上,因此我们可以在将其转换成一个普通的string&,这个显然是安全的!!
4.特殊用途的语言特性
<0>:默认实参
默认实参就是函数在调用时,我们没有给这个参数一个实参,它所默认的参数!!!
我们可以给函数设定默认的参数,但是需要注意的是:某个参数设定过默认参数后,其后的参数都要设有默认参数。道理其实也很简单,如果后面的参数没有设定默认参数,在调用时很容易产生二义性。例如:
int max(int i, int j); // 没有默认参数
int max(int i, int j = 0); // j设定默认参数为0
int max(int i = 0, int j); // 错误,i后面的j没有设定默认参数
<1>:使用带有默认参数的函数
PS:只能省略尾部的默认参数
window = screen(, , '#'); // 错误
<2>:内联函数
使用一般的函数有个潜在的缺点:调用函数比求等价表达式一般要低效一些。因为在大多数的机器中:调用函数前,需要保存寄存器,并在返回时回复;可能还要拷贝实参;程序转向另一个地方执行。
而内联函数可以有效地避免函数调用的开销。
例如,我们指定shorterString函数为内联函数(inline):
inline const string &shorterString(const string &s1, const string &s2){};
// 调用
cout << shorterString(s1, s2) << endl;
// 编译过程中会展开为如下:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
<3>:constexpr函数
constexpr函数是指能用于常量表达式的函数。例如
constexpr int new_sz(){return 42;}
constexpr int foo = new_sz(); // 正确
我们也允许constexpr函数返回一个非常量值。
constexpr size_t scale(size_t cnt) {return new_ze() * cnt};
当其实参是常量表达式时,返回的是常量表达式,反之亦然。
5.调试工具
<0>:assert预处理宏
assert(expr); // 当表达式expr值为0时,输出相关信息并终止程序执行,否则什么也不做
<1>:NDEBUG预处理变量
assert的行为依赖于NDEBUG的预处理变量的状态。如果定义了NDEBUG,那么assert什么也不做。
#define NDEBUG 0 // 0可以是任何数,只要定义了,就可以避免检查assert的各种开销
NDEBUG可以编写自己的调试代码:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
// _ _func_ _是编译器第一的一个局部静态变量,用于存放函数名
cerr << _ _func_ _ << " : array size is " << size << endl;
#endif
}
PS:几个预处理器定义的名字
_ _func_ _:存放函数名 // 编译器定义的,其他四个是预处理器定义的
_ _FILE_ _:存放函数名的字符串字面量
_ _LINE_ _:存放当前行号的整型字面量
_ _TIME_ _:存放文件编译时间的字符串字面量
_ _DATE_ _:存放文件编译日期的字符串字面量
6.函数指针
声明:只需要将函数名替换成指针就可以了
bool (*pf)(const string &, const string &); // pf指向一个需要两个const string &参数的返回bool类型的函数。
使用函数指针:我们把函数名当作一个值使用时,该函数名会自动转换成指针。
pf = lengthCompare;
pf = &lengthCompare; // 这两者是等价的
bool b1 = pf("hello", "goodbey");
bool b2 = (*pf)("hello", "goodbey"); // 这两个也是等价的
重载函数的指针:必须清晰的界定是哪一个函数的指针
void ff(int *);
void ff(unsigned int);
void (*pf)(unsigned int) = ff; //指向后一个
函数指针形参:
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// 第三个形参是函数类型,会自动转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
// 这个是上面一个的等价声明
类型别名和decktype能够简化其使用:
// func 和 func2是函数类型
typedef bool func(const string &, const string &);
typedef decltype(lengthCompare) func2; // 等价类型
// funcp 和 funcp2 是指向函数的指针
typedef bool(*funcp)(const string &, const string &);
typedef decltype(lengthCompare) *funcp2; // 等价类型
返回指向函数的指针:
使用别名将其简化:
using F = int (int *, int ); // F是函数类型,不是指针
using PF = int(*)(int *, int ); // FF是指针类型
PF f1(int); // 正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int ); // 错误:F是函数类型,f1不能返回一个函数
F *f1(int ); // 正确
出于完整性考虑,我们还能使用尾置返回类型:
auto f1(int) -> int (*)(int*, int);
使用auto和decltype用于函数指针类型:
注意:我们将decltype用于某个函数时,它返回的是函数类型而不是指针类型,所以我们必须加上“*”号。
string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);
decltype(sumLength) *getFunc(const string &); // 我们可以根据参数决定返回上面两个函数中的哪一个。
wwww,函数这一块终于给进行一次总结了,之前老感觉这也有点印象,那好像也不会,之后就可以更方便的查阅了,多多复习,多多总结,实话说,这么一记还真的比单纯的看书理解的深刻,可能我看书太马虎了吧!!!gogogo!