目录
内联函数
常规函数调用的执行过程:(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函数与右值匹配
函数模板
关于模板类或者模板函数声明和定义的相关问题
特点:模板并不创建任何函数,只有在遇到了调用时,才会用指定类型去进行创建函数,开辟空间。最终的代码不包含任何模板,只包含了为程序而生成的是函数。使用模板的好处是,使多个函数定义更简单,更可靠
关于编译:在进行编译即 gcc -S时会进行编译,并生成一个汇编文件(以s结尾),此时关于模板的编译会进行两次:1.对模板本身的代码进行编译 2.对模板的调用代码进行编译
模板的局限性和具体化
局限性:
编写的模板函数可能无法处理某些类型,例如如下代码
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页有详细介绍
模板类和友元函数的正确打开方式
这些也是我在别的地方看到的,因为实验室项目没涉及到这里,所以没考虑过这个问题,正确的打开方式如下
#include<iostream>
using namespace std;
template<typename T>
class Person;
template<typename T>
void show(Person<T>& p);
template<typename T>
class Person {
public:
Person(T age);
friend void show<T>(Person<T>& p);
public:
T mAge;
};
template<typename T>
Person<T>::Person(T age) {
this->mAge = age;
}
template<typename T>
void show(Person<T>& p) {
cout << p.mAge << endl;
}
int main() {
Person<int> p(20);
show(p);
return 0;
}