0.目录
- extern
- static
- volatile
- const
- inline
- explicit
c++11:
- decltype
- auto
- 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
-
局部变量
static
修饰局部变量时,改变了局部变量的存储位置及其生命周期,未改变其作用域。- 内存中的位置:从原来的栈中存放改为静态存储区
- 初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
- 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束
- 生命周期:在
main
函数之前初始化,在程序退出时销毁
-
全局变量
static
修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量。- 内存中的位置:始终为静态存储区
- 初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
- 作用域:全局静态变量在声明他的文件之外是不可见的,准确地讲从定义之处开始到文件结尾
- 生命周期:在
main
函数之前初始化,在程序退出时销毁
函数
static
修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。-
成员变量
用static
修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例,静态成员属于整个类,而不属于某个对象。 因此,static
成员必须在类外进行初始化(初始化格式:int base::var=10;
),而不能在构造函数内进行初始化,不过也可以用const
修饰static
数据成员在类内初始化。- 不要试图在头文件中定义(初始化)静态数据成员,在大多数的情况下,这样做会引起重复定义这样的错误。即使加上
#ifndef
#define
#endif
或者#pragma once
也不行 - 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以
- 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用
- 不要试图在头文件中定义(初始化)静态数据成员,在大多数的情况下,这样做会引起重复定义这样的错误。即使加上
-
成员函数
- 用
static
修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this
指针,因而只能访问类的static
成员变量 - 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。例如可以封装某些算法,比如数学函数,如
ln
,sin
,tan
等等,这些函数本就没必要属于任何一个对象,所以从类上调用感觉更好,比如定义一个数学函数类Math
,调用Math::sin(3.14);
还可以实现某些特殊的设计模式:如Singleton
; - 当
static
成员函数在类外定义时不需要加static
修饰符 - 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含
this
指针
- 用
不可以同时用
const
和static
修饰成员函数
C++编译器在实现const
的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*
。但当一个成员为static的时候,该函数是没有this
指针的。也就是说此时const
的用法和static
是冲突的。我们也可以这样理解:两者的语意是矛盾的。static
的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const
的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。隐藏
当同时编译多个文件时,所有未加static
前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static
可以用作函数和变量的前缀,对于函数来讲,static
的作用仅限于隐藏。
3.volatile
用来修饰变量,表明某个变量的值可能会随时被外部改变,因此这些变量的存取不能被缓存到寄存器,每次使用需要重新读取。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
假如有一个对象A里面有一个boolean
变量a
,值为true
,现在有两个线程T1
,T2
访问变量a
,T1
把a
改成了false
后T2
读取a
,T2
这时读到的值可能不是false
,即T1
修改a
的这一操作,对T2
是不可见的。发生的原因可能是,针对T2
线程,为了提升性能,虚拟机把a
变量置入了寄存器(即C语言中的寄存器变量),这样就会导致,无论T2
读取多少次a
,a
的值始终为true
,因为T2
读取了寄存器而非内存中的值。声明了volatile
或synchronized
后,就可以保证可见性,确保T2
始终从内存中读取变量,T1
始终在内存中修改变量。总结:防止脏读,增加内存屏障。
4.const
- 定义变量为只读变量,不可修改
- 修饰函数的参数和返回值(后者应用比较少,一般为值传递)
-
const成员函数
(只需要在成员函数参数列表后加上关键字const
,如char get() const
)可以访问const成员变量
和非const成员变量
,但不能修改任何变量。在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const成员函数
。 -
const对象
只能访问const成员函数
,而非const对象
可以访问任意的成员函数,包括const成员函数
。即对于class A
,有const A a
;那么a
只能访问A
的const成员函数
。而对于A b
,b
可以访问任何成员函数。
使用const
关键字修饰的变量,一定要对变量进行初始化
5.inline
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline
修饰符,表示为内联函数。
- 不能包含复杂的控制语句,
while
、switch
- 内联函数本身不能直接调用自身
- 如果内联函数的函数体过大,编译器会自动把这个内联函数变成普通函数
- 编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,省去函数的调用的开销,提高效率
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
类型有时候会跟初始值的类型并不完全一样,编译器会适当的改变结果类型使得其更符合初始化规则,如float
和double
编译器似乎会更偏爱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
, 编译器几乎没有机会做优化。
事实上,编译器甚至会让代码变得更臃肿、庞大:- 栈必须被保存在回退表中;
- 所有对象的析构函数必须被正确的调用(按照对象构建相反的顺序析构对象);
- 编译器可能引入新的传播栅栏(propagation barriers)、引入新的异常表入口,使得异常处理的代码变得更庞大;
- 内联函数的异常规格(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;
}