C++ Primer Plus第八章总结与测试

it2025-04-07  27

目录

内联函数内联与宏 引用初始化按引用传递交换两个变量的值临时变量生成的时机 尽可能使用const引用于返回值为何要返回引用最后一点 默认参数函数重载定义匹配问题重载引用参数 函数模板模板的局限性和具体化局限性:显式具体化实例化部分排序规则

内联函数

常规函数调用的执行过程:(1)执行到函数调用指令时,存储该指令的内存地址,并将函数参数复制到堆栈中(2)调到函数起点的内存单元开始执行,并将返回值放入寄存器中(3)执行完毕之后跳回到之前保存的指令处 内联函数的适用场合:函数长度短,被多次调用但不能是递归 使用方法:在函数的声明和定义前加上关键字 inline

内联与宏

#include<iostream> #include<string> using namespace std; #define SQU(X) X * X int main() { int a = 1; cout<<SQU(a + 2); system("pause"); return 0; }

这里的输出结果是5,因为#define并不是通过传递参数实现的,而是通过文本替换来实现的,将a + 2替换为X,所以真正参与计算的就是 1 + 2 * 1 + 2结果为5 如果使用C语言的宏执行了类似函数的功能,应该考虑换成C++的内联函数 inline double SQU(double x) {return x * x; }

引用

初始化

声明引用变量时必须进行初始化初始化之后不允许修改

下面通过代码进行说明

#include<iostream> using namespace std; int main() { int rats = 101; int & rodents = rats; cout<<"rats address:"<<&rats<<" value:"<<rats<<endl; cout<<"rodents address:"<<&rodents<<" value:"<<rodents<<endl; int bunnies = 50; rodents = bunnies; cout<<"after change\n"; cout<<"rats address:"<<&rats<<" value:"<<rats<<endl; cout<<"rodents address:"<<&rodents<<" value:"<<rodents<<endl; cout<<"bunnies address:"<<&bunnies<<" bunnies:"<<bunnies<<endl; rodents++; cout<<"after add\n"; cout<<"rats address:"<<&rats<<" value:"<<rats<<endl; cout<<"rodents address:"<<&rodents<<" value:"<<rodents<<endl; cout<<"bunnies address:"<<&bunnies<<" bunnies:"<<bunnies<<endl; system("pause"); return 0; } 对应的输出为 rats address:0x61fe14 value:101 rodents address:0x61fe14 value:101 after change rats address:0x61fe14 value:50 rodents address:0x61fe14 value:50 bunnies address:0x61fe10 bunnies:50 after add rats address:0x61fe14 value:51 rodents address:0x61fe14 value:51 bunnies address:0x61fe10 bunnies:50

从上面的输出中我们可以看到,虽然乍一看我们让rodents重新赋值为bunnies,但是当我们再进行输出以后发现rats和rodents的值都发生了改变,并且二者地址一样且皆不同于bunnies,类似于一个const型的指针

按引用传递

交换两个变量的值

我们都知道,如果要编写函数交换两个变量的值,函数的参数需要是指针,现在也可以通过传递引用实现同样的效果 那么关于按值传递,按址传递以及按引用传递三者的不同点在哪里呢? 还是通过代码进行解释

#include<iostream> using namespace std; void swap_value(int a1, int b1) { cout<<"swap_value a1 address:"<<&a1<<" b1 address:"<<&b1<<endl; swap(a1, b1); } void swap_reference(int& a1, int& b1) { cout<<"swap_reference a1 address:"<<&a1<<" b1 address:"<<&b1<<endl; swap(a1, b1); } void swap_point(int* a1, int* b1) { cout<<"swap_point a1 address:"<<&a1<<" b1 address:"<<&b1<<endl; swap(*a1, *b1); } int main() { int a = 10, b = 20; cout<<"a address:"<<&a<<" b address:"<<&b<<endl; cout<<"a:"<<a<<" b:"<<b<<endl; swap_value(a, b); cout<<"after swap_value:"<<"a:"<<a<<" b:"<<b<<endl; swap_reference(a, b); cout<<"after swap_reference:"<<"a:"<<a<<" b:"<<b<<endl; swap_point(&a, &b); cout<<"after swap_point:"<<"a:"<<a<<" b:"<<b<<endl; system("pause"); return 0; } 对应的输出是: a address:0x61fe1c b address:0x61fe18 a:10 b:20 swap_value a1 address:0x61fdf0 b1 address:0x61fdf8 after swap_value:a:10 b:20 swap_reference a1 address:0x61fe1c b1 address:0x61fe18 after swap_reference:a:20 b:10 swap_point a1 address:0x61fdf0 b1 address:0x61fdf8 after swap_point:a:10 b:20

通过上面的代码我们可以清楚地发现,当我们调用swap_reference时,用a和b两个实参去初始化两个引用型变量的形参,由于引用就是一个别名,所以这里可以看到&a1 == &a, &b1 == &b,因为他们本质上都是一个内存地址,只不过是不同的名字,但是当我们调用swap_point时,就会发现虽然他也可以完成交换功能,但实际上还是会在栈区开辟出两个指针型的变量的空间,分别用a和b的地址对其进行初始化,所以这里&a1 != &a, &b1 != &b

临时变量

如果实参与引用参数不匹配,C++将生成临时变量,当且仅当参数为const的引用时

#include<iostream> using namespace std; void test(const int& a) { cout << "666\n"; } void tes2t(int& a) { cout << "666\n"; } int main() { test(6.0); //ok test2(6.0); //无法通过编译 system("pause"); return 0; }

生成的时机

实参的类型正确,但不是左值实参的类型不正确,但可以进行转换 左值指的是,可被引用的数据对象,例如,变量,数组元素,结构成员,引用和解除引用的指针,常规变量和const变量都可视为左值,但常规变量属于可修改的左值,const变量属于不可修改的左值 实例参见如下代码,在vscode中可通过编译 #include<iostream> using namespace std; double test(const double& ra) { return ra * ra; } int main() { double side = 3.0; double* pd = &side; double& rd = side; long edge = 5L; double lens[4]{2.0, 5.0, 10.0, 12.0}; double c1 = test(side); double c2 = test(lens[2]); double c3 = test(rd); double c4 = test(*pd); double c5 = test(edge);//规则2 double c6 = test(7.0);//规则1 double c7 = test(side + 10.0);//规则1 system("pause"); return 0; }

尽可能使用const

理由如下:

使用const可避免无意修改参数导致错误使用const使函数能够处理const和非const实参,否则只能接受非const数据使函数能正确生成并使用临时变量

引用于返回值

如果如书中P266所说,如果返回值类型是一个对象,那么会把整个结构复制到一个临时位置,再将这个拷贝复制给要承接这个返回值的变量中,还是通过代码来举例子

#include<iostream> using namespace std; class student { public: int number; student(const student& stu) { cout << "copy instruct called\n"; } student() { cout << "instruct called\n"; } student test(); }; student student::test() { student st; return st; } int main() { student st; student st1(st.test()); student st2 = st.test(); system("pause"); return 0; }

这里我只是简单的重写了拷贝构造函数,上面这段代码是为了讲解当函数返回一个对象时会发生什么,对于vs2019来说,会调用拷贝构造函数,但是对于vscode来说则不会这么做 所以输出结果如下

vs2019的输出 instruct called //初始化st instruct called //st.test时生成的对象 copy instruct called //用st.test生成的对象去初始化st1 instruct called //st.test时生成的对象 copy instruct called //用st.test生成的对象去初始化st2 vscode的输出 instruct called instruct called instruct called

关于C++中拷贝构造函数的调用时机,参考这篇文章

为何要返回引用

传统的返回机制与按值传递参数类似,计算关键字return后面的表达式,并将结构复制到一个临时的位置,再将这个结果拷贝给承接他的地方,但当返回值为引用时,直接把返回值复制到承接的变量中(可能会调用拷贝构造函数或者是赋值构造函数) 书P267 对于按值传递函数参数来说,会创建一个局部变量用实参去初始化,待函数结束之后会进行空间的释放,也就是在栈上开辟的临时空间被实参初始化,函数结束再出栈 书P207 总的来说就是,返回引用的效率要高一些 注意点:返回引用的函数实际上是被引用的变量的别名,而且要注意不要返回指向临时变量的引用,因为函数执行完之后临时变量将不复存在,那还引用个锤子,试图引用已释放的内存,这就很离谱

最后一点

假设实参的类型与引用类型不匹配,但可被转化为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参来初始化形参

默认参数

注意点1:只在声明中使用默认参数 注意点2:必须从右向左添加默认值,因为形参和实参匹配是从左到右的

函数重载

定义

函数名称相同,但是参数数目或者参数类型不同

匹配问题

在使用被重载的函数时,需要在函数调用中使用正确的参数类型,说明代码如下

#include<iostream> using namespace std; void print(int i, int j) { cout <<"function 1\n"; } void print(double i, int j) { cout <<"function 1\n"; } void print(long i, int j) { cout <<"function 1\n"; } int main() { unsigned int a = 10; print(a, 6); system("pause"); return 0; }

这时无法进行任何匹配,因为这三个print均可把a进行转换,vscode中报错:error: call of overloaded ‘print(unsigned int&, int)’ is ambiguous,意思就是匹配的重载函数太多了,找不到合适的.

另外:编译器在检查函数参数列表时,把引用类型和类型本身不做区分

重载引用参数

这对于不同引用类型的重载很有用 void sink(double& r1); void sank(const double& r2); void sunk(double && r3); 对于第一个sink函数,他只能和与可修改的左值参数匹配,第二个sank函数与可修改的左值,const左值,或者是右值参数匹配,第三个sunk函数与右值匹配

函数模板

关于模板类或者模板函数声明和定义的相关问题 特点:模板并不创建任何函数,只有在遇到了调用时,才会用指定类型去进行创建函数,开辟空间。最终的代码不包含任何模板,只包含了为程序而生成的是奇函数。使用模板的好处是,使多个函数定义更简单,更可靠

模板的局限性和具体化

局限性:

编写的模板函数可能无法处理某些类型,例如如下代码

struct student { int a, b; }; template<typename T> void SWAP(T& t1, T& t2) { t1 += t2; } student stu1, stu2; SWAP(stu1, stu2); 这个时候如何进行加法操作

解决方法有两种,第一个是重载相应的操作符,第二种就是模板的显式具体化

显式具体化

可以提供一个具体化的函数定义–称为显式具体化,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,直接使用不再去寻找模板 对于上面的例子,对应的显式具体化代码为, template关键字后要有一对尖括号

template<> void SWAP<student> (student& st1, student& st2) { st1.a += st2.a; st1.b += st2.b; }

实例化

代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。在上面的例子中,如果我们通过如下代码 int a = 10, b = 2; SWAP(a, b); 那么将导致编译器生成SWAP()的一个实例,该实例使用int类型。模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式称为隐式实例化。

与之对应的就有显式实例化,他的语法很简单 template void SWAP(int, int) 注意这里template后面是没有尖括号的

还可通过在程序中使用函数来创建显式实例化 template<typename T> T Add(Ta, Tb) { return a + b; } int m = 6; double x = 10.2; cout << Add<double>(x, m) << endl; 这里的模板与函数调用Add(x, m)不匹配, 因为该模板要求两个函数参数的类型相同。 但通过使用Add<double>(x, m)可强制为double类型实例化, 并将参数m强制转换为double类型

但这种做法如果是 int m = 5; double x = 10.2; SWAP< double>(m, x); 这样写将为double生成一个显式实例化,但是有SWAP指定的形参为引用类型,无法指向int变量,原因在上面关于临时变量的创建中有提到

隐式实例化,显示实例化和显式具体化统称为具体化 优先级:非模板函数 > 具体化 > 常规模板

部分排序规则

指的就是找出最具体模板的规则,这部分内容书上290页有详细介绍

最新回复(0)