C++-部分关键字的作用

0.目录

  1. extern
  2. static
  3. volatile
  4. const
  5. inline
  6. explicit

c++11:

  1. decltype
  2. auto
  3. noexcept

1.extern

extern置于变量或函数前,用于标示变量或函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。它主要有两个作用:

  • 当它与“C”一起连用的时候,如:extern "C" void fun(int a,int b);则告诉编译器在编译fun这个函数时候按着C的规矩去翻译,而不是C++的(这与C++的重载有关,C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同)
  • extern不与“C”在一起修饰变量或函数时,如:extern int g_Int;它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用。记住它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

2.static

  1. 局部变量
    static修饰局部变量时,改变了局部变量的存储位置及其生命周期,未改变其作用域。

    • 内存中的位置:从原来的栈中存放改为静态存储区
    • 初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
    • 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束
    • 生命周期:在main函数之前初始化,在程序退出时销毁
  2. 全局变量
    static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量。

    • 内存中的位置:始终为静态存储区
    • 初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
    • 作用域:全局静态变量在声明他的文件之外是不可见的,准确地讲从定义之处开始到文件结尾
    • 生命周期:在main函数之前初始化,在程序退出时销毁
  3. 函数
    static修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。

  4. 成员变量
    static修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例,静态成员属于整个类,而不属于某个对象。 因此,static成员必须在类外进行初始化(初始化格式:int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。

    • 不要试图在头文件中定义(初始化)静态数据成员,在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行
    • 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以
    • 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用
  5. 成员函数

    • static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量
    • 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。例如可以封装某些算法,比如数学函数,如lnsintan等等,这些函数本就没必要属于任何一个对象,所以从类上调用感觉更好,比如定义一个数学函数类Math,调用Math::sin(3.14);还可以实现某些特殊的设计模式:如Singleton
    • static成员函数在类外定义时不需要加static修饰符
    • 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针
  6. 不可以同时用conststatic修饰成员函数
    C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

  7. 隐藏
    当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏。

3.volatile

用来修饰变量,表明某个变量的值可能会随时被外部改变,因此这些变量的存取不能被缓存到寄存器,每次使用需要重新读取。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

假如有一个对象A里面有一个boolean变量a,值为true,现在有两个线程T1T2访问变量aT1a改成了falseT2读取aT2这时读到的值可能不是false,即T1修改a的这一操作,对T2是不可见的。发生的原因可能是,针对T2线程,为了提升性能,虚拟机把a变量置入了寄存器(即C语言中的寄存器变量),这样就会导致,无论T2读取多少次aa的值始终为true,因为T2读取了寄存器而非内存中的值。声明了volatilesynchronized后,就可以保证可见性,确保T2始终从内存中读取变量,T1始终在内存中修改变量。总结:防止脏读,增加内存屏障。

4.const

  • 定义变量为只读变量,不可修改
  • 修饰函数的参数和返回值(后者应用比较少,一般为值传递)
  • const成员函数(只需要在成员函数参数列表后加上关键字const,如char get() const)可以访问const成员变量非const成员变量,但不能修改任何变量。在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const成员函数
  • const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数。即对于class A,有const A a;那么a只能访问Aconst成员函数。而对于A bb可以访问任何成员函数。

使用const关键字修饰的变量,一定要对变量进行初始化

5.inline

在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。

  • 不能包含复杂的控制语句,whileswitch
  • 内联函数本身不能直接调用自身
  • 如果内联函数的函数体过大,编译器会自动把这个内联函数变成普通函数
  • 编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,省去函数的调用的开销,提高效率

6.explicit

只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).防止类构造函数的隐式自动转换

7.decltype

从表达式中推断出要定义变量的类型,在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。

decltype(f()) sum = x;// sum的类型就是函数f的返回值类型,在这里编译器并不实际调用f函数,而是分析f函数的返回值作为sum的定义类型。
  • 如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const引用&在内):
const int ci = 42, &cj = ci;    
decltype(ci) x = 0;   // x 类型为const int    
decltype(cj) y = x;   // y 类型为const int&  
  • 如果表达式的内容是解引用操作,则decltype将得到引用&类型。解引用指针可以得到指针所指对象,而且还可以给这个对象赋值。因此decltype(*p)的结果类型就是int&:
int i = 42, *p = &i, &r = i;  
  
decltype(i) x1 = 0;       //因为 i 为 int ,所以 x1 为int  
auto x2 = i;              //因为 i 为 int ,所以 x2 为int  
  
decltype(r) y1 = i;       //因为 r 为 int& ,所以 y1 为int&  
auto y2 = r;              //因为 r 为 int& ,但auto会忽略引用,所以 y2 为int  
  
decltype(r + 0) z1 = 0;   //因为 r + 0 为 int ,所以 z1 为int,  
auto z2 = r + 0;          //因为 r + 0 为 int ,所以 z2 为int,  
  
decltype(*p) h1 = i;      //这里 h1 是int&, 原因后面讲  
auto h2 = *p;             // h2 为 int. 
  • 如果decltype使用的是一个不加括号的变量,那么得到的结果就是这个变量的类型。如果给这个变量加上一个或多层括号,那么编译器会把这个变量当作一个表达式看待,变量是一个可以作为左值的特殊表达式,所以这样的decltype就会返回引用&类型:
int i = 42;    
//decltype(i)   int  类型  
//decltype((i)) int& 类型  
  • =赋值运算符返回的是左值的引用。换句话意思就是说decltype(i = b)返回类型为i类型的引用
  • decltype还有一个用途就是在c++11引入的后置返回类型

8.auto

auto能让编译器通过初始值来进行类型推演,从而获得定义变量的类型。

  • auto定义的变量必须有初始值。
  • 使用auto能在一个语句中声明多个变量,但是一个语句在声明多个变量的时候,只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须是一样的,在这里一定要区别数据类型和类型修饰符:
int i = 3;  
auto a = i,&b = i,*c = &i;//正确: a初始化为i的副本,b初始化为i的引用,c为i的指针.  
auto sz = 0, pi = 3.14;//错误,两个变量的类型不一样。  
  • 编译器推断出来的auto类型有时候会跟初始值的类型并不完全一样,编译器会适当的改变结果类型使得其更符合初始化规则,如floatdouble编译器似乎会更偏爱double
  • auto会忽略引用&
int i = 0 ,&r = i;//定义一个整数i,并且定义r为i的应用.  
auto a = r; //这里的a为为一个整数,其值跟此时的i一样.
  • auto一般会忽略掉顶层const,但底层const会被保留下来,比如当初始值是一个指向常量的指针时:
int i = 0;  
const int ci = i, &cr = ci;  //ci 为整数常量,cr 为整数常量引用   
auto a = ci;     // a 为一个整数, 顶层const被忽略  
auto b = cr;     // b 为一个整数,顶层const被忽略  
auto c = &ci;    // c 为一个整数指针.  
auto d = &cr;    // d 为一个指向整数常量的指针(对常量对象区地址是那么const会变成底层const)  
  • 如果希望推断出auto类型是一个顶层const,需要明确指出:
const auto f = ci;
  • 还可以将引用的类型设为auto,此时原来的初始化规则仍然适用(用于引用声明的const都是底层const):
auto &g = ci; //g是一个整数常量引用,绑定到ci。  
auto &h = 42; // 错误:非常量引用的初始值必须为左值。  
const auto &j = 42; //正确:常量引用可以绑定到字面值。  

9.noexcept

用作说明符

在函数后加上noexcept,代表这个函数不会抛出异常,如果抛出异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序就会终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。

void swap(Type& x, Type& y) throw()   //C++11之前
{ x.swap(y); }
void swap(Type& x, Type& y) noexcept  //C++11
{ x.swap(y); }

//如果操作x.swap(y)不发生异常,那么函数swap(Type& x, Type& y)一定不发生异常
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y)))
{ x.swap(y); }

//std::pair中的移动分配函数(move assignment)
//如果类型T1和T2的移动分配(move assign)过程中不发生异常,那么该移动构造函数就不会发生异常
pair& operator=(pair&& __p)
noexcept(__and_<is_nothrow_move_assignable<_T1>,
                is_nothrow_move_assignable<_T2>>::value)
{
    first = std::forward<first_type>(__p.first);
    second = std::forward<second_type>(__p.second);
    return *this;
}

void f() noexcept; // 函数 f() 不抛出
void (*fp)() noexcept(false); // fp 指向可能抛出的函数
void g(void pfa() noexcept);  // g 接收指向不抛出的函数的指针
// typedef int (*pf)() noexcept; // 错误

使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率,以下情形鼓励使用noexcept

  • 移动构造函数(move constructor)
  • 移动分配函数(move assignment)
  • 析构函数(destructor),在新版本的编译器中,析构函数是默认加上关键字noexcept的。
  • 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。

在不是以上情况或者没把握的情况下,不要轻易使用noexcept

throw()和noexcept的区别在于程序运行时的行为和编译器优化的结果:

  • throw()
    如果函数抛出异常,异常处理机制会进行栈回退,寻找一个(或多个)catch语句并检测catch可以捕捉的类型,如果没有匹配的类型,std::unexpected()会被调用。 但是std::unexpected()本身也可能抛出异常, 如果std::unexpected()抛出的异常对于当前的异常规格是有效的, 异常传递和栈回退会继续进行。 之后调用std::teminate()

    这意味着,如果使用throw, 编译器几乎没有机会做优化。
    事实上,编译器甚至会让代码变得更臃肿、庞大:

    1. 栈必须被保存在回退表中;
    2. 所有对象的析构函数必须被正确的调用(按照对象构建相反的顺序析构对象);
    3. 编译器可能引入新的传播栅栏(propagation barriers)、引入新的异常表入口,使得异常处理的代码变得更庞大;
    4. 内联函数的异常规格(exception specification)可能无效的。
  • noexcept
    std::teminate()函数会被立即调用,而不是调用std::unexpected()。因此,在异常处理的过程中,编译器不会回退栈,这为编译器的优化提供了更大的空间。

简而言之,如果你知道你的函数绝对不会抛出任何异常,应该使用noexcept, 而不是throw().

用作运算符

noexcept 运算符进行编译时检查,若表达式声明为不抛出任何异常则返回 true,它可用于函数模板的noexcept 说明符中,以声明函数将对某些类型抛出异常,但不对其他类型抛出。

noexcept(表达式)
//返回 bool 类型的纯右值

noexcept 运算符不对表达式求值。若表达式含有至少一个下列潜在求值的语言构造,则结果为 false

  • 调用无不抛出异常说明的任何类型的函数,除非它是常量表达式
  • throw 表达式
  • 目标类型为引用类型,且转换需要运行时检查的 dynamic_cast 表达式
  • 实参类型为多态类类型的 typeid 表达式

所有其他情况下结果是 true

若 表达式 的潜在异常集合为空则结果为 true,否则为 false。(C++17 起)

void test() {}
void test_noexcept() noexcept(true) {}
void test_noexcept_false() noexcept(false) {  }

class Base{
public:
    virtual void f() {}
};
class Test :public Base {
};

int main(int argc, char **argv)
{
    std::cout << noexcept(test()) << std::endl;                       \\ false
    std::cout << noexcept(test_noexcept()) << std::endl;              \\ false
    std::cout << noexcept(test_noexcept_false()) << std::endl;        \\ true
    std::cout << noexcept(throw) << std::endl;                        \\ false

    Test test;
    Base& base = test;
    std::cout << noexcept(dynamic_cast<Test&>(base)) << std::endl;     \\ false
    std::cout << noexcept(typeid(base)) << std::endl;                  \\ false

    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容