深入C++11笔记

C++11的诞生

为了制定C++11标准,C++标准委员会针对其遇到的各种困难做出了各种应对的策略,这些策略也构成了C++11标准的语言基础。相比于C++98/03,一些特性在C++11下就不再使用了,因此它们被弃用了。总体而已,C++11的设计目标如下 :

  1. 更适用于系统开发及库开发;
  2. 更加容易学习;
  3. 保证语言的稳定性和兼容性,能够兼容C++03及C语言。

相比于C++98/C++03,C++11在如下方面得到显著增强:

  1. 支持本地并行编程,引入了相应的内存模型,线程,原子操作等;
  2. 通过统一初始化表达式、auto、decltype、移动语义等来统一对泛型编程的支持;
  3. 更好地支持系统编程,引入相关的常量表达式constexpr等;
  4. 更好地支持库的构建 ,引入内联命名空间,右值引用等。

就C++11设计目标来说,在设计C++11标准时,C++标准委员会主要专注于如下的理念:

  1. 保持语言的稳定性和兼容性;
  2. 更倾向于使用库而不是扩展语言来实现特性;
  3. 更倾向于通过通用的手段来实现特性,而不是特殊手段;
  4. 既能支持新手,又能支持专家;
  5. 增强类型的安全性;
  6. 增强执行性能代码和操纵硬件的能力;
  7. 能够改善人们的思维方式;
  8. 融入编程现实。

保持稳定性和兼容性

在C++11中,设计者总是在保证不破坏原有设计的情况下,增加新的特性,以充分保证语言的稳定性和兼容性。

保持与C99兼容

相比于C++98/03,C++11支持了C99的以下特性:

  1. C99中的预定义宏:C99新增了预定义宏,__STD_HOSTED__、__STDC__、__STDC_VERSION__、__STDC_ISO_10646__。在C++11中,也支持了C99这些预定义宏。通过支持这些预定义宏,可以使得C++11能够在同一套代码中支持多个目标平台。
  2. __func__预定义标识符:编译器会隐式地在函数定义后定义该标识符,标识所在函数的名称。因此,通过该标识符,对代码调试具有很大帮助。
  3. 不定参数宏定义及__VA_ARGS__:可以将宏定义参数列表中的最后一个参数写成省略号,然后在宏定义的实现部分通过__VA_ARGS__宏来替代省略号。
  4. 宽窄字符串连接:C++11定义多种类型的字符。宽窄字符串连接时,C++11会将窄字符串转成宽字符串,然后再与宽字符串连接。

整型

C++11对整型也做了适当的调整,以更好地保持与C语言的兼容性。在C++11中,主要针对两类整型做了调整:

  1. long long 整型:在C++11中,该整型大小至少有64位,具体取值依赖于所在的平台。
  2. 编译器扩展的整型:C++11允许编译器扩展自有的整型,为了保证稳定性和兼容性,C++11对这些扩展整型的使用规则做了一些限制。和标准整型一样,有符号和无符号的扩展整型占同样大小的内存空间,同时也要遵循整型间的隐式转换,即小整型向大整型转换,有符号向无符号转换。

断言

断言通过判断条件是否总是为真来排除在设计的逻辑上不应该存在的情况。它作为一种常用的调试手段,并不是正常程序所必需的。在C++98/03中,断言使用的是assert宏,其继承于C语言。然而,这个断言宏有个缺点,它的发生是在运行时。某些情况下,我们需要静态时的断言,即能够在编译时就进行断言。例如:在模版函数内进行断言,

template <typename T, typename U>
void func(T &a, U &b)
{
	assert(sizeof(a) == sizeof(b));
	memcpy(&a, &b, sizeof(a));
}

在这个场景下,assert断言只有在模板函数func被调用时才会发生。显然,这个断言其实不需要等到运行时才可以判断其真伪,它可以在模板函数func实例化时就可以进行判断了,即编译时就已经知道断言是否成立。为此,C++11引入了静态断言。在C++11之前,也有一些利用语言规则来实现静态断言的方法。例如可以利用除零的方法,

#define assert_static(e) \
do { \
	enum {assert_static__ =  1 / (e) }; \
} while(0)

上面的宏可以通过除零来导致编译器报错。然而这种方法存在缺点,不能快速定位代码哪里产生了bug,同时对新手不友好。因此C++11引入了static_assert断言,它接收两个参数,第一个参数是断言表达式,能够在编译时可以计算出结果的表达式,也即是常量表达式。第二个参数是字符串,可以用于提示程序员哪里出现了问题。

异常

在C++11之前,如果想要通过声明来指出代码可能抛出的异常类型,那么可以通过动态异常声明,如下:

void func () throw(int, double) 
{
}

然而,在C++11中,动态异常声明被弃用了。反而,C++11引入了noexcept异常声明。相比于基于异常机制的throw声明,noexcept可以有效阻止异常的传播和扩散,因为当出现异常时,编译器直接调用std::terminate函数来终止程序的运行,因此效率也较高。noexcept声明有两种格式,如下:

  1. 在函数后面直接加上noexcept关键字,
void func() noexcept
{
}
  1. 在函数后面加上带常量表达式作为参数的noexcept操作符,通过常量表达式的结果来决定是否可能抛出异常。
void func() noexcept(常量表达式)
{
}

noexcept声明可以用于模板函数,例如:

template <typename T>
void func() noexcept(noexcept(T()))
{
}

上面的例子,可以根据条件来实现noexcept修饰的版本和无noexcept修饰的版本。

此外,在自定义类中,析构函数默认是不抛出异常的,因此它被声明为noexcpet(true),与析构函数相关的delete函数也是一样的。通过这样,可以减少由于析构函数抛出异常导致的安全问题。

初始化

相比于C++98,C++11支持了就地初始化非静态成员变量。在C++11之前,C++只支持了常量静态成员就地初始化,而且类型还得是整型或者枚举型的静态成员。除了初始化列表外,C++11允许使用等号=或者花括号{}初始化非静态成员变量,但不允许使用(),例如:

class A
{
public:
	int a = 1;  // 合法
	double b {1.2}; // 合法
	B b(0); // 非法
};

在C++11中,初始化列表和就地初始化并不冲突,可以就同一成员变量使用就地初始化进行初始化,也可以使用初始化列表进行初始化。但是初始化列表初始化优先于就地初始化。

自定义类型

非静态成员的大小

在C++11之前,只有静态成员或者对象的实例才有可能对其成员进行sizeof操作。因此,为了获取非静态成员的大小,程序员需要掌握一定的编程技巧,例如可以将通过类型转换,将0强转为自定义类型的指针,然后通过指针的解引用获取相应的成员变量,最后通过sizeof操作符获取该变量的大小。

sizeof(((A *)0)->member);

在C++11中,程序员无需这样的技巧,直接可以使用sizeof来作用于类成员的表达式。

友元

在C++中,friend关键字可以声明一个类的友元。友元可以无视类的成员访问控制属性,也就是,无论成员是private、public、还是protected,友元都是可以访问。所以在某种程度上说,友元其实是破坏了类的封装性。在C++11中,针对友元做了改进,在将一个类声明为另一个类的友元时,不再需要加上class关键字。这也是得为类模板声明友元成为了可能。

template <typename T>
class B
{
	friend T; // 在C++11是合法的
} 

继承与派生

待定。

函数重载

针对成员函数重载,C++11引入了final和override两个描述符。通过这两个关键字,程序员对成员函数重载拥有更大的控制权,继而更好地控制类的派生行为。其中final描述符表示该成员函数不能再被深层的派生类重载,override描述符表示派生类必须重载其基类的同名函数。这两个描述符具体用法如下:

class Base
{
public:
	virtual void func1() = 0;
	virtual void func2() const;
};

class Derive : public Base
{
public:
	void func1() final 
	{
	}
	void func2() const override
	{
	}
};

在上面的代码中,类Derive重载了基类Base的成员函数func1和func2,其中func1不能在被类Derive的派生类所重载,而Derive的func2 由于后面加上了override描述符,其命名一定要和基类的func2同名,这包括函数名称,参数列表,返回值类型,函数的常量性等都要一致。

模板

C++11标准对模板做了一些改善

模板参数

虽然在C++98中,类模板是允许有默认的模板参数的,但是它不允许函数模板有默认的参数,例如下面的代码:

template <typename T = double> // C++98 合法
class A
{
}; 

template <typename T = double> // C++98 不合法
void func(T a) 
{
}

不过在C++11中,函数模版也可以有默认的参数,也就是上面的代码在C++11中都是合法的。但是与类模板不一样,在为类模板的多个默认模板参数指定默认值时,必须遵循“从右到左”的顺序,例如以下的代码,

template <typename T, typename U = int> // 合法
class A
{
};
template <typename T = int, typename U> // 不合法
class B
{
};

但在为函数模板的默认模板参数指定默认值时,就不需要遵循“从右到左”的顺序。函数模版会首先根据函数的实参来函数模板的参数,如果能够推导出来,那么函数模板的默认参数值就不会用了,否则,就需要为相应的模板参数指定默认值。

template <typename T = int, typename U>
void func1(T a = 0, U b = 0)
{
}
int main ()
{
	func1(1.2); // 合法
	func1(); // 不合法,推导不出U是哪个类型
}

此外,C++11还对模板实参做了调整,使得匿名类型和局部类型可以作为模板的实参,而这在C++98是不允许的。

template <typename T>
void func1 (T a )
{
}
typedef struct 
{
} A;
int main()
{
	struct B
	{
	};
	A a;
	B b;
	func1(a); // C++98不合法, C++11合法
	func1(b); // C++98不合法, C++11合法		
}

外部模板

C++11 引入了外部模板,它主要作为编译时优化的手段。由于使用模板,编译器很容易产生重复代码,所以在链接时,需要对这些冗余代码进行去重。例如代码:

\\ header.h
template <typename T>
void func(T a)
{
}

\\ src1.c
#include "header.h"
void func1()
{
	func<int>(1);
}

\\ src2.c
#include "header.h"
void func2()
{
	func<int>(1);
}

在上面代码中,文件src1.c 和src2.c 会分别对模板函数func进行实例化为 func,也就是src1.o 和 src2.o 包含了相同的代码 func(int),因此在链接器需要移除重复实例化代码。为了减少编译时间和链接时间,C++11引入了外部模版,其使用依赖于显式实例化,如下:

\\ header.h
template <typename T>
void func(T a)
{
}

\\ src1.cpp
#include "header.h"
template void func<int>(int); // 显式实例化

\\ src2.cpp
extern template void func<int>(int); // 声明外部模板

此时编译器只有在src1.o保存一份func(int)实例化代码,而src2.o不会再生成实例化代码。在使用外部模板时,需要保证一定在某个编译单元对模板进行了显式实例化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值