第二章 内联函数、引用、const
内联函数(inline function)
- 使用inline修饰函数的声明或者实现,可以使其变成内联函数
- 建议声明和实现都增加inline修饰
- 但是注意:有默认参数的函数,只能加在声明上,不能加载实现上。
- 特点
- 编译器会将函数调用直接展开为函数体代码
- 将VS设置为release
- 点击项目属性->属性->C/C++->优化设置为“已禁用”,而且内联函数扩展设置为“任何适用项”
- 编译,然后将.exe拖入到IDA可以看到,main函数中的汇编代码
-
sum函数没有添加inline时,有调用: call sum
push 14h ; b push 0Ah ; a call ?sum@@YAHHH@Z ; sum(int,int)
-
有inline修饰时,没有调用call sum 语句
mov eax, 0Ah add eax, 14h push eax
- 可以减少函数调用的开销
-
会增大代码体积
//内联函数声明 inline int sum(int a, int b); int main() { //内联函数 cout << sum(10, 20) << endl; //上面的本质就是: //cout << 10 +20 << endl; getchar(); return 0; } //内联函数实现 inline int sum(int a, int b) { return a + b; }
- 编译器会将函数调用直接展开为函数体代码
- 注意
- 尽量不要内联超过10行代码的函数
- 有些函数即使声明为inline,也不一定会被编译器内联,比如递归函数
- inline仅仅是建议编译器内联,但是如果代码很多编译器不一定内联。
- 递归是没办法内联的。
- 内联函数使用场合
- 函数代码不是很多
- 函数的调用频率较高
内联函数与宏
- 内联函数与宏都是替换,那么他们有什么区别呢?
- 内联函数和宏,都可以减少函数调用的开销
- 都在编译阶段替换掉
- 对比宏,内联函数多了语法检测和函数特性
-
思考以下代码的区别
#define sum(x) (x + x) inline int sum(int x) { return x + x; } int a = 10; sum(a++); //宏:简单的替换 a++ + a++ //内联函数,只有一次++ int x = a++; sum(x);
#
pragma once
- 我们用VS新建一个.h文件,这个头文件内部会默认一个
#pragma once
,那么它是什么作用呢? - 我们经常使用
#ifndef、#define、#endif
来防止头文件的内容被重复包含 #pragma once
可以防止整个文件的内容被重复包含- 区别
#ifndef、#define、#endif
受C\C++标准的支持,不受编译器的任何限制- 有些编译器不支持#pragma once(较老编译器不支持,如GCC 3.4版本之前),兼容性不够好
#ifndef、#define、#endif
可以针对一个文件中的部分代码,而#pragma once只能针对整个文件
引用(Reference)
- 在C语言中,使用指针(Pointer)可以间接获取、修改某个变量的值
- 在C++中,使用引用(Reference)可以起到跟指针类似的功能
- 注意点
- 引用相当于是变量的别名(基本数据类型、枚举、结构体、类、指针、数组等,都可以有引用)
- 对引用做计算,就是对引用所指向的变量做计算
- 在定义的时候就必须初始化,一旦指向了某个变量,就不可以再改变,“从一而终”
- 可以利用引用初始化另一个引用,相当于某个变量的多个别名
- 不存在【引用的引用、指向引用的指针、引用数组】
//定义一个枚举 enum Season { spring, summer, fall, winner }; //定义一个结构体 struct Student { int age; }; int main() { //指针 int a = 10; int *age = &a; *age = 20; cout << "a=" << a << endl; //引用 //定义了一个引用,相当于score的别名 int score = 80; int &rscore = score; rscore = 90; cout << "score = " << score << endl; //枚举引用 Season season; Season &rseason = season; rseason = winner; cout << "season = " << season << endl; //结构体引用 Student student; Student &rstudent = student; rstudent.age = 25; cout << "student.age = " << student.age << endl; //指针引用 int x; int y; int *p = &x; //给指针变量p取一个别名rp,rp就相当于p int *&rp = p; *rp = 20; cout << "x = " << x << endl; //数组引用 int array[] = { 10,20,30 }; //数组的引用必须用(),而且[]内的元素必须确定 int (&rarray)[3] = array; //这么写就相当于一个“引用数组:一个数组中有3个元素,每个元素是一个整形引用,但是数组中是不允许放引用的” //int &array[3] cout << "rarray[2]=" << rarray[2] << endl; //这么是错的,定义的时候必须初始化 //int &rxx; //一旦指向了某个变量就不能改变 int m = 10; int &rm = m; int n = 9; //这仅仅是将n的值赋值给rm,即m = n,并没有改变rm的指向 rm = n; //这么写就不对了,&rm单独写,就是取地址的意思,只有在定义的时候才是引用 //&rm = n; cout << "rm= " << rm << endl; cout << "m= " << m << endl; cout << "n= " << n << endl; //用一个引用,初始化另一个引用 int o = 10; int &ro = o; int &rro = ro; rro = 20; cout << "o= " << o << endl; //不存在指向引用的引用 int ss = 10; int &rss = ss; //错误 //int &&rss = rss; //不存在指向引用的指针 //(int &)*pp; //不存在引用数组 //int &array[3] = { 0 }; getchar(); return 0; }
- 引用存在的价值之一:比指针更安全、函数返回值可以被赋值
- 安全:只能指向一个变量
//函数返回值赋值 int func() { return 6; } int &func2(){ int a = 10; return a; } //交换2个数的值 //使用指针 void swap(int *a,int *b){ int temp = *a; *a = *b; *b = temp; } //使用引用 void swaps(int &a, int &b) { int temp = a; a = b; b = temp; } //主函数调用 int main() { int v1 = 10; int v2 = 20; //swap(&v1, &v2); swaps(v1, v2); cout << "v1=" << v1 << endl; cout << "v2=" << v2 << endl; //很显然是错误的 //func() = 6; //函数返回的是一个引用,可以被赋值.不过这样写没有意义,函数中的a并不会改变,函数调用完a被销毁。 func2() = 20; }
const
- const是常量的意思,被其修饰的变量不可修改
- 如果修饰的是类、结构体(的指针),其成员也不可以更改
//const修饰的变量必须在定义的时候初始化 //const int age; const int age = 10; //整个结构体、成员变量都不可以修改 const Student student = { 10 }; //student.age = 20; Student student2 = {40}; //student = student2; //指针也不允许修改 const Student *stu3 = &student2; //stu3->age = 30;
-
以下5个指针分别是什么含义?
//const 右边的东西就是常量 int age = 10; //*p0 是常量,p0可以改 const int *p0 = &age; //效果同上 int const *p1 = &age; //p2 不能改 *p2 可以改 int *const p2 = &age; //*p3 是常量,p3是常量 const int* const p3 = &age; //同上 int const * const p4 = &age;
- 上面的指针问题可以用以下结论来解决:
- const修饰的是其右边的内容
常引用(Const Reference)
- 引用可以被const修饰,这样就无法通过引用修改数据了,可以称为常引用
- const必须写在&符号的左边,才能算是常引用
int age = 10; //这样修饰不能改rage const int &rage = age; //这样修饰能改 rage,没有意义 //int & const rage = age; //这样就不能改了 rage = 30;
- const引用的特点
- 可以指向临时数据(常量、表达式、函数返回值等)
- 可以指向不同类型的数据
- 作为函数参数时(此规则也适用于const指针(就是把&换成*))
- 可以接受const和非const实参(非const引用,只能接受非const实参)
- 就是函数参数有const修饰时,参数既可以接收const修饰的实参,也能接收不带const修饰的实参
- 反之,只能接收不带const修饰的实参
- 可以跟非const引用构成重载
- 但是如果参数不是引用类型的话就不可以
- 参数是指针类型也可以
#include<iostream> using namespace std; int func() { return 10; } //只能接受没有const修饰的实参 int sum1(int &a, int &b) { return a + b; } //不管是否带const修饰的实参都可以接收 int sum(const int &a, const int &b) { cout << "sum-const" << endl; return a + b; } //sum重载 int sum(int &a, int &b) { cout << "sum" << endl; return a + b; } //参数不是引用类型的话就不可以重载:这么就不可以重载,会产生二义,sum(v1, v2); /* int sum(int a, int b) { cout << "sum" << endl; return a + b; } */ //参数是指针类型也可以重载:不会产生二义 int sum(int* a, int* b) { cout << "sum" << endl; return *a + *b; } int main() { int a = 10; int b = 20; //通常引用是不可以指向常量 //int &rage = 40; //但是常引用可以 const int &rage = 50; //通常是不可以指向表达式 //int &rrage = a + b; //常引用可以 const int &rrage = a + b; //通常是不可以指向函数返回值 //int &rrrage = func(); //但是常引用可以 const int &rrrage = func(); //通常是不可以指向不同类型数据 //double ® = a; //常引用可以 const double ® = a; //const 修饰函数参数 int v1 = 10; int v2 = 20; //调用sum sum(v1, v2); const int v3 = 10; const int v4 = 20; //这么写是不可以的,必须把sum函数的参数也用const修饰 //sum1(v3, v4); //调用sum-const sum(v3, v4); getchar(); return 0; }
- 可以接受const和非const实参(非const引用,只能接受非const实参)
-
当常引用指向了不同类型的数据时,会产生临时变量,即引用指向的并不是初始化时的那个变量
//示例1 int age = 10; //const double &rage = age; const int &rage = age; age = 20; //此时rage 也被修改为20了,说明此时rage引用仍然指向age cout << "rage = " << rage << endl; //示例2 int age = 10; const double &rage = age; age = 20; //此时rage仍然为10 cout << "rage = " << rage << endl; cout << "age = " << age << endl; //因此说明此时的rage引用并没有指向age了 //通过反汇编可以看出生成了一个 新的变量,rage指向了新的变量
数组的引用
//数组的引用
int array[] = { 1,2,3,4 };
//方法1
int(&rarray)[4] = array;
rarray[1] = 10;
//方法2:通过指针引用
//int *p = array; //C语言
int * const &p = array;
表达式赋值
//表达式
int a = 10;
int b = 20;
//表达式也可以赋值,赋值给了a
(a = b) = 30;
//打印结果:a = 30;b= 20;
cout << "a= " << a << endl;
cout << "b=" << b << endl;
//赋值给了小者
(a < b ? a : b) = 4;
引用的本质
- 引用的本质就是指针,只是编译器削弱了它的功能,所以引用就是弱化了的指针
- 一个引用占用一个指针的大小
- 汇编分析
-
源码
int age = 10; int &rage = age; rage = 20;
-
汇编
int age = 10; //将0Ah这个值存到[ebp-0Ch]这个内存,占用4个字节(dword),此时age就是[ebp-0Ch] //[ebp-0Ch] =0Ah mov dword ptr [ebp-0Ch],0Ah int &rage = age; //将[ebp-0Ch]这个地址值存入到eax寄存器,注意,eax此时存入的是地址值 // eax = ebp-0Ch lea eax,[ebp-0Ch] //将eax寄存器中的地址值 存入到 [ebp-18h] 内存中占据4个字节。[ebp-18h]这块内存地址就是rage // [ebp-18h] = eax = ebp-0Ch mov dword ptr [ebp-18h],eax rage = 20; //将[ebp-18h]内存中取4个字节的数据([ebp-0Ch]),存入到eax寄存器中 // eax = [ebp-18h] = ebp-0Ch mov eax,dword ptr [ebp-18h] //将14h这个常量存储到eax寄存器所指的内存中取,即存入到[ebp-0Ch]内存中 // [eax] = [ebp-0Ch] = 14h mov dword ptr [eax],14h
- 总结
int &rage = age;
这句代码可以看出- 将age的地址(ebp-0Ch)存储到寄存器eax中,然后将将eax赋值给[ebp-18h]这个内存空间
- [ebp-18h]就是引用变量rage,说白了rage存储的是age的地址,那么也就说明rage本质是一个指针变量而且大小为跟指针变量一样
rage = 20;
本质就是找到rage([ebp-18h])存储的地址,然后根据这个地址找到内存([ebp-0Ch])存入20.即根据rage存储的地址找到相应的内存存入数据。
-
指针反汇编
int age = 10; int *rage = &age; *rage = 20; //反汇编 // int age = 10; mov dword ptr [ebp-0Ch],0Ah //int *rage = &age; lea eax,[ebp-0Ch] mov dword ptr [ebp-18h],eax //*rage = 20; mov eax,dword ptr [ebp-18h] mov dword ptr [eax],14h
- 可以发现跟引用的反汇编一模一样
-