C++语言特性——关键字(static、volatile、extern、const、mutable、inline)

注意: 本内容为摘抄网上的学习资料,作为个人笔记使用,如有侵权, 立刻删除。

C++语言特性

1.关键字

(1)static

static全局变量和普通全局变量
面试高频指数:★★★☆☆

相同点:

  • 存储方式:普通全局变量和 static 全局变量都是静态存储方式(编译时分配内存)。

不同点:

  • 作用域:普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件(编译模块)内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。

例如在a.c中定义了static int a=10;
那么在b.c中用extern int a是拿不到a的值得,a的作用域只在a.c中。

  • 初始化

    • static修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值(比如整型的0)初始化它。
    • 用static声明局部变量-------局部变量指在代码块 {} 内部定义的变量,只在代码块内部有效(作用域),用static声明局部变量时,则改变变量的存储方式(生命期),使变量成为静态的局部变量,即编译时就为变量分配内存,直到程序退出才释放存储单元。这样,使得该局部变量有记忆功能,可以记忆上次的数据不过由于仍是局部变量,因而只能在代码块内部使用(作用域不变)
    • 修饰成员变量,在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员对多个对象来说,静态数据成员只存储一处,供所有对象共用
  • 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。

    如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可

  • 修饰成员函数

    静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名

    在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。

    如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

返回函数中静态变量的地址会发生什么?

面试高频指数:★★☆☆☆

#include <iostream>
using namespace std;

int * fun(int tmp){
    static int var = 10;
    var *= tmp;
    return &var;
}

int main() {
    cout << *fun(5) << endl;
    return 0;
}

/*
运行结果:
50
*/

说明:上述代码中在函数 fun 中定义了静态局部变量 var,使得离开该函数的作用域后,该变量不会销毁,返回到主函数中,该变量依然存在,从而使程序得到正确的运行结果。但是,该静态局部变量直到程序运行结束后才销毁,浪费内存空间。

静态变量什么时候初始化

  • 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

  • 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

  • 而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

static 静态成员变量:

面试高频指数:★★★★★

  1. 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。
  2. 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象
    疑问:派生类是什么?
  3. 静态成员变量可以作为成员函数的参数,而普通成员变量不可以
#include <iostream>
using namespace std;

class A
{
public:
    static int s_var;
    int var;
    void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
    void fun2(int i = var);   //  error: invalid use of non-static data member 'A::var'
};
int main()
{
    return 0;
}
  1. 静态数据成员的类型可以是所属类的类型而普通数据成员的类型只能是该类类型的指针或引用

疑问:指针 引用 分别是什么意思?

#include <iostream>
using namespace std;

class A
{
public:
    static A s_var; // 正确,静态数据成员
    A var;          // error: field 'var' has incomplete type 'A'
    A *p;           // 正确,指针
    A &var1;        // 正确,引用
};

int main()
{
    return 0;
}
static 静态成员函数:
  1. 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针,必须通过类名才能访问。静态成员函数做为类作用域的全局函数。
    疑问:this指针是什么?怎么用?

  2. 静态成员函数不能声明成虚函数(virtual)、const 函数volatile 函数
    疑问:虚函数、const 函数、volatile 函数 是什么?

(2) volatile

volatile 的作用?是否具有原子性,对编译器有什么影响?

面试高频指数:★★☆☆☆

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化(意思就是告诉编译器不要忽略这样的对象)。

  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值),保证对特殊地址的稳定访问

  • const 可以是 volatile (如只读的状态寄存器)

  • 指针可以是 volatile

  • volatile不具有原子性。

  • 注意:

    • 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象
    • 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰
    • C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

什么情况下一定要用 volatile, 能否和 const 一起使用?

面试高频指数:★★☆☆☆

使用 volatile 关键字的场景:

  • 当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;

  • 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。

volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。

关键字volatile有什么含意?

答案:一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

volatile关键字

volatile int i = 10; 
  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
  • const 可以是 volatile (如只读的状态寄存器)
  • 指针可以是 volatile

const, volatile区别

(1)const含义是“请做为常量使用”,而并非“放心吧,那肯定是个常量”,是不可修改的只读变量。
volatile的含义是“请不要做自以为是的优化,这个值可能变掉的”,而并非“你可以修改这个值”。

(2)const只在编译期有用,在运行期无用

const在编译期保证在C的“源代码”里面,没有对其修饰的变量进行修改的地方(如有则报错,编译不通过),而运行期该变量的值是否被改变则不受const的限制。

volatile在编译期和运行期都有用

在编译期告诉编译器:请不要做自以为是的优化,这个变量的值可能会变掉;

在运行期:每次用到该变量的值,都直接从内存中取该变量的值。

(3)const, volatile同时修饰一个变量

合法性

“volatile”的含义并非是“non-const”,volatile 和 const 不构成反义词,所以可以放一起修饰一个变量。

同时修饰一个变量的含义:表示一个变量在程序编译期不能被修改且不能被优化;在程序运行期,变量值可修改,但每次用到该变量的值都要从内存中读取,以防止意外错误。

(3)extern

请你来说一说extern“C”

参考回答:

  • 被 extern 限定的函数或变量是 extern 类型的
  • extern "C" 修饰的变量和函数是按照 C 语言方式编译和链接的
    extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

extern “C” 使用

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

extern C为什么需要?

原因:c和c++ 对同一个函数经过编译后生成的函数名是不同的,由于C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果在c++ 中调用一个使用c语言编写的模块中的某个函数,那么c++ 是根据c++ 的名称修饰方式来查找并链接这个函数,那么就会发生链接错误。

extern 关键字作用

基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

也就是说extern有两个作用。第一,当它与"C"一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的。

第二,当extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围关键字,其声明的函数和变量可以在本模块或其他模块中使用,记住**它是一个声明不是定义!**也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

(4)const

面试高频指数:★★★☆☆

作用:
  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
  • const 修饰函数参数,使得传递过来的函数参数的值不能改变。
  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量
在类中的用法:
const 成员变量:
  1. const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。

  2. const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。

const 成员函数:

不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。
不能调用非常量成员函数,以防修改成员变量的值。
疑问:下面的代码 A(int tmp) : var(tmp) {}没看懂

#include <iostream>
using namespace std;

class A
{
public:
	int var;
	A(int tmp) : var(tmp) {}
	void c_fun(int tmp) const // const 成员函数
	{
		var = tmp; // error: assignment of member 'A::var' in read-only object. 在 const 成员函数中,不能修改任何类成员变量。		
		fun(tmp); // error: passing 'const A' as 'this' argument discards qualifiers. const 成员函数不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
	}

	void fun(int tmp)
	{
		var = tmp;
	}
};

int main()
{
    return 0;
}

请你来说一下C++里是怎么定义常量的?常量存放在内存的哪个位置?

常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化

  • 对于局部对象,常量存放在栈区,
  • 对于全局对象,常量存放在全局/静态存储区。
  • 对于字面值常量,常量存放在常量存储区。
    疑问?字面值常量是什么?

const作用 “只读”

  1. const修饰变量,以下两种定义形式在本质上是一样的。它的含义是:const修饰的类型为TYPE的变量value是不可变的。
TYPE const ValueName = value;

const TYPE ValueName = value;
  1. 修饰指针
  • 指向常量的指针(pointer to const)
  • 自身是常量的指针(常量指针,const pointer)
  1. 修饰引用
  • 指向常量的引用(reference to const)如果用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  • 没有 const reference,因为引用只是对象的别名,引用不是对象,不能用 const 修饰
  1. 修饰成员函数,说明该成员函数内不能修改成员变量。const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

const 使用

(为了方便记忆可以想成)被 const 修饰(在 const 后面)的值不可改变,如下文使用例子中的 p2p3

实际上const和*的优先级相同,且是从右相左读的,即“右左法则”
疑问:下面的 ** 已经让我懵逼

比如int*p;//读作p为指针,指向int,所以p为指向int的指针

int*const p;//读作p为常量,是指针,指向int,所以p为指向int的常量指针, p不可修改

int const *p;//p为指针,指向常量,为int,所以p为指向int常量的指针, *p不可修改

int ** const p; //p为常量,指向指针,指针指向int,所以p为指向int型指针的常量指针,p不可修改

const int **p;//指向常量指针的指针

int const**p; //p为指针,指向指针,指针指向常量int,所以p为指针,指向一个指向int常量的指针, **p为int,不可修改

int * const *p ; //p为指针,指向常量,该常量为指针,指向int,所以p为指针,指向一个常量指针,*p为指针,不可修改

int ** const *p; //p为指针,指向常量,常量为指向指针的指针,p为指针,指向常量型指针的指针,*p为指向指针的指针,不可修改

int * const **p; //p为指针,指向一个指针1,指针1指向一个常量,常量为指向int的指针,即p为指针,指向一个指向常量指针的指针, **p为指向一个int的指针,不可修改


== 为什么? const A &q = a; // 指向常对象的引用 ==

// 类
class A
{
private:
    const int a;                // 常对象成员,可以使用初始化列表或者类内初始化

public:
    // 构造函数
    A() : a(0) { };
    A(int x) : a(x) { };        // 初始化列表

    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};

void function()
{
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数
    const A a;                  // 常对象,只能调用常成员函数
    const A *p = &a;            // 指针变量,指向常对象
    const A &q = a;             // 指向常对象的引用  

    // 指针
    char greeting[] = "Hello";      //字符串hello保存在栈区,可以通过greeting去修改
    const char * arr = "123"; //字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
    char * brr = "123"; //字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值
    const char crr[] = "123";

//这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
//确实,经过简单测试const char crr[]="123"; crr[1]='5';结果报错
//error: assignment of read-only location ‘crr[1]’
    
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 指针变量,指向字符数组常量(const 后面是 char,说明不能通过p2修改greeting,但是greeting在栈上可以通过其它方式修改,比如下标,和p1来修改)
    char* const p3 = greeting;          // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变,即指针不能指向其它地址,但可以修改其中的值)
    //比如*(p2+1)='c'; 则报错assignment of read-only location ‘*(p2 + 1u)’
    const char* const p4 = greeting;    // 自身是常量的指针,指向字符数组常量
}

函数中使用const

(1)const修饰函数参数

a.传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)

void function(const int Var);

b.参数指针所指内容为常量不可变

void function(const char* Var);

c.参数指针本身为常量不可变(也无意义,因为char* Var也是形参)

void function(char* const Var);

d.参数为引用,为了增加效率同时防止修改。修饰引用参数时:

void function(const Class& Var); //引用参数在函数内不可以改变

void function(const TYPE& Var); //引用参数在函数内为常量不可变

这样的一个const引用传递和最普通的函数按值传递的效果是一模一样的,他禁止对引用的对象的一切修改,唯一不同的是按值传递会先建立一个类对象的副本, 然后传递过去,而它直接传递地址,所以这种传递比按值传递更有效.另外只有引用的const传递可以传递一个临时对象,因为临时对象都是const属性, 且是不可见的,他短时间存在一个局部域中,所以不能使用指针,只有引用的const传递能够捕捉到这个家伙.

(2)const 修饰函数返回值

const修饰函数返回值其实用的并不是很多,它的含义和const修饰普通变量以及指针的含义基本相同。

const int fun1() //返回一个常数,这个其实无意义,因为参数返回本身就是赋值。

const int * fun2() //返回一个指向常量的指针变量,使用如下

const int *pValue = fun2(); //我们可以把fun2()看作成一个变量,即指针内容不可变。


int* const fun3() //返回一个指向变量的常指针

int * const pValue = fun2(); //我们可以把fun2()看作成一个变量,即指针本身不可变。
const与define
const
  • const 常量限定符,声明的常量只读,不允许修改
  • 在 C++ (不是 C) 中可以用 const 值声明数组长度
  • 对于全局 const 值,C++ 中默认是内部链接(跟 static 一样只允许在本文件内可见),而不是 C 中的默认外部链接,若想在其他文件中使用必须在其他文件中重新定义或将 const 值放在头文件中(默认是外部链接放在头文件中编译可能会出现错误,默认是内部链接就不会出错)
define
  • 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。

  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。

  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。

  • 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const 定义的常量可以进行调试。

C++ 中推荐使用 const 代替 #define 声明常量

  • const 能够明确指定常量类型
  • const 能够用于更复杂的数据类型(如:数组,结构体和类)
  • const 标识符遵循变量的作用域规则,可以创建作用域为全局(仅在本文件中使用)、命名空间、函数或数据块的常量

(5)mutable

mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置

(6)inline

面试高频指数:★★★☆☆

作用:

inline 是一个关键字,可以用于定义内联函数内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。

特征

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于,却比宏多了类型检查,真正具有函数特性;宏是什么?
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

使用方法:

  1. 类内定义成员函数默认是内联函数
    在类内定义成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数,代码如下:
#include <iostream>
using namespace std;

class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }    //这句不懂??
    void fun(){ 
        cout << var << endl;
    }
};

int main()
{    
    return 0;
}
  1. 类外定义成员函数,若想定义为内联函数,需用关键字声明
    当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。
#include <iostream>
using namespace std;

class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }    
    void fun();
};

inline void A::fun(){
    cout << var << endl;
}

int main()
{    
    return 0;
}

另外,可以在声明函数和定义函数的同时加上 inline;也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。

inline 函数工作原理

面试高频指数:★★☆☆☆

  • 内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。

  • 普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。

编译器对 inline 函数的处理步骤

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

inline 优缺点

优点:
  1. inline定义的内联函数,函数代码被放入符号表中,在使用时进行替换(内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度),效率很高。

  2. 类的内联函数也是函数。编绎器在调用一个内联函数,首先会检查参数问题,保证调用正确,像对待真正函数一样,消除了隐患及局限性。内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。

  3. inline可以作为类的成员函数。在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。

  4. 内联函数在运行时可调试,而宏定义不可以

缺点:
  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大(比如函数体内有循环),那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。这种情况编译器可能会自动把它作为非内联函数处理
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

内联函数一般可以:

  1. 加快程序的执行速度;

  2. 可能减小可执行文件的大小;

  3. 可能增加可执行文件的大小;

  4. 可能降低执行速度。

1和3很好理解,在编译时期内联函数能将代码直接写入其被调用的地方,这样就减少了入栈出栈的时间消耗,但是如果调用内联函数的地方过多,代码量也会随之增加,增加了可执行文件的大小。

2为什么正确呢?是因为如果调用普通函数的话编译器可能会产生更多的代码来实现压、出寄存器的代码,对于简单的内联函数会这样。但如果优化器能顺序集成消除大量冗余代码的话,对大函数也同样适用。

4呢?如果可执行文件过大,会频繁的出现内存的换入换出操作,会使执行速度下降。

虚函数(virtual)可以是内联函数(inline)吗?什么是虚函数?

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

虚函数内联使用

#include <iostream>  
using namespace std;
class Base
{
public:
	inline virtual void who()
	{
		cout << "I am Base\n";
	}
	virtual ~Base() {}
};
class Derived : public Base
{
public:
	inline void who()  // 不写inline时隐式内联
	{
		cout << "I am Derived\n";
	}
};

int main()
{
	// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
	Base b;
	b.who();

	// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。  
	Base *ptr = new Derived();
	ptr->who();

	// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
	delete ptr;
	ptr = nullptr;

	system("pause");
	return 0;
} 

inline 与 typedef 与 define

typedef
  • 类型重命名可以写在函数外部,同样也可以函数内部,它们的作用域不同,可以提高代码的可读性
  • typedef 可以分别为基本类型重命名、指针类型重命名、结构体类型重命名和函数指针类型重命名
  • typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
define
  • 原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
  • 作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
  • enum给int型常量起名字,typedef给数据类型起名字,宏定义也可以看做一种重命名
指针的操作:typedef 和 #define 在处理指针时不完全一样
#include <iostream>
#define INTPTR1 int *
typedef int * INTPTR2;

using namespace std;

int main()
{
    INTPTR1 p1, p2; // p1: int *; p2: int
    INTPTR2 p3, p4; // p3: int *; p4: int *

    int var = 1;
    const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
    const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。
    
    return 0;
}

C++ 中推荐使用 inline 代替 #define 声明函数

  • C++ 中使用 inline 定义内联函数(C 语言中一般是使用 #define 定义宏函数)
  • 宏函数是简单的文本替换,不是真正的传参数,如果不注意运算顺序很容易出错,C++ 使用 inline 定义内联函数,比定义宏函数可靠,inline 定义的内联函数是真正的传递参数,C++ 中 inline 可用于常规函数也可用于类方法
  • 宏函数的一个优点是无类型,可用于任意类型,运算都是有意义的,在 C++ 中可创建内联函数模板实现这个功能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值