第二章 内联函数、引用、const

内联函数(inline function)

  1. 使用inline修饰函数的声明或者实现,可以使其变成内联函数
    1. 建议声明和实现都增加inline修饰
    2. 但是注意:有默认参数的函数,只能加在声明上,不能加载实现上。
  2. 特点
    1. 编译器会将函数调用直接展开为函数体代码
      1. 将VS设置为release
      2. 点击项目属性->属性->C/C++->优化设置为“已禁用”,而且内联函数扩展设置为“任何适用项”
      3. 编译,然后将.exe拖入到IDA可以看到,main函数中的汇编代码
      4. sum函数没有添加inline时,有调用: call sum

         push    14h             ; b
         push    0Ah             ; a
         call    ?sum@@YAHHH@Z   ; sum(int,int)
        
      5. 有inline修饰时,没有调用call sum 语句

         mov     eax, 0Ah
         add     eax, 14h
         push    eax
        
    2. 可以减少函数调用的开销
    3. 会增大代码体积

       //内联函数声明
       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;
       }
      
  3. 注意
    1. 尽量不要内联超过10行代码的函数
    2. 有些函数即使声明为inline,也不一定会被编译器内联,比如递归函数
      1. inline仅仅是建议编译器内联,但是如果代码很多编译器不一定内联。
      2. 递归是没办法内联的。
  4. 内联函数使用场合
    1. 函数代码不是很多
    2. 函数的调用频率较高

内联函数与宏

  1. 内联函数与宏都是替换,那么他们有什么区别呢?
    1. 内联函数和宏,都可以减少函数调用的开销
    2. 都在编译阶段替换掉
    3. 对比宏,内联函数多了语法检测和函数特性
  2. 思考以下代码的区别

     #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

  1. 我们用VS新建一个.h文件,这个头文件内部会默认一个#pragma once,那么它是什么作用呢?
  2. 我们经常使用#ifndef、#define、#endif来防止头文件的内容被重复包含
  3. #pragma once可以防止整个文件的内容被重复包含
  4. 区别
    1. #ifndef、#define、#endif受C\C++标准的支持,不受编译器的任何限制
    2. 有些编译器不支持#pragma once(较老编译器不支持,如GCC 3.4版本之前),兼容性不够好
    3. #ifndef、#define、#endif可以针对一个文件中的部分代码,而#pragma once只能针对整个文件

引用(Reference)

  1. 在C语言中,使用指针(Pointer)可以间接获取、修改某个变量的值
  2. 在C++中,使用引用(Reference)可以起到跟指针类似的功能
  3. 注意点
    1. 引用相当于是变量的别名(基本数据类型、枚举、结构体、类、指针、数组等,都可以有引用)
    2. 对引用做计算,就是对引用所指向的变量做计算
    3. 在定义的时候就必须初始化,一旦指向了某个变量,就不可以再改变,“从一而终”
    4. 可以利用引用初始化另一个引用,相当于某个变量的多个别名
    5. 不存在【引用的引用、指向引用的指针、引用数组】
     //定义一个枚举
     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;
     }
    
  4. 引用存在的价值之一:比指针更安全、函数返回值可以被赋值
    1. 安全:只能指向一个变量
     //函数返回值赋值
     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

  1. const是常量的意思,被其修饰的变量不可修改
    1. 如果修饰的是类、结构体(的指针),其成员也不可以更改
      //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;
    
  2. 以下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;
    
  3. 上面的指针问题可以用以下结论来解决:
    1. const修饰的是其右边的内容

常引用(Const Reference)

  1. 引用可以被const修饰,这样就无法通过引用修改数据了,可以称为常引用
    1. const必须写在&符号的左边,才能算是常引用
     int age = 10;
     //这样修饰不能改rage
     const int &rage = age;
     //这样修饰能改 rage,没有意义
     //int & const rage = age;
     //这样就不能改了
     rage = 30;
    
  2. const引用的特点
    1. 可以指向临时数据(常量、表达式、函数返回值等)
    2. 可以指向不同类型的数据
    3. 作为函数参数时(此规则也适用于const指针(就是把&换成*))
      1. 可以接受const和非const实参(非const引用,只能接受非const实参)
        1. 就是函数参数有const修饰时,参数既可以接收const修饰的实参,也能接收不带const修饰的实参
        2. 反之,只能接收不带const修饰的实参
      2. 可以跟非const引用构成重载
        1. 但是如果参数不是引用类型的话就不可以
        2. 参数是指针类型也可以
       #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 &reg = a;
       	//常引用可以
       	const double &reg = 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;
       }
      
  3. 当常引用指向了不同类型的数据时,会产生临时变量,即引用指向的并不是初始化时的那个变量

     //示例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;

引用的本质

  1. 引用的本质就是指针,只是编译器削弱了它的功能,所以引用就是弱化了的指针
  2. 一个引用占用一个指针的大小
  3. 汇编分析
    1. 源码

       int age = 10;
       int &rage = age;
       rage = 20;
      
    2. 汇编

       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
      
    3. 总结
      1. int &rage = age;这句代码可以看出
        1. 将age的地址(ebp-0Ch)存储到寄存器eax中,然后将将eax赋值给[ebp-18h]这个内存空间
        2. [ebp-18h]就是引用变量rage,说白了rage存储的是age的地址,那么也就说明rage本质是一个指针变量而且大小为跟指针变量一样
      2. rage = 20;本质就是找到rage([ebp-18h])存储的地址,然后根据这个地址找到内存([ebp-0Ch])存入20.即根据rage存储的地址找到相应的内存存入数据。
    4. 指针反汇编

       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
      
      1. 可以发现跟引用的反汇编一模一样
Table of Contents