1 C++11 新特性内容

  1. 革新
  2. 语法糖
  3. 标准库的扩充
  4. 老语法bug的修复

    1.1 革新

  5. 模板:很多革新都是围绕模板展开

  6. 模板元编程:
  7. decltype关键字:编译器推导表达式类型
  8. 可变参数模板:(class…\typename…)元编程利器
  9. 函数模板的默认模板(语法糖)
  10. using 与模板的别名
  11. 右值引用(&&)
  12. move语义(std::move)
  13. 完美转发(std::forward(t))
  14. 以上几乎都与模板有点联系?

    1.2 语法糖

    语法糖:相当于汉语中的成语。用更简练的言语表达较复杂的含义。在得到广泛接受的情况之下,可以提升交流的效率。
    在计算机科学中,语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言:操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯。

  15. auto关键字(自动类型推导)

  16. lambda表达式
  17. 初始化参数列表/统一初始化方式

    1.3 标准库的扩充

  18. 智能指针。特性?区别?实现原理?

  19. STL标准库。元组tuple。
  20. std::thread。用法?多线程?

    2 类型推导 auto & decltype

    类型的推导(auto & decltype)是在编译期就完成的,仍是静态类型
    【auto的背景】:
    一般来说, 在把一个表达式或者函数的返回值赋给一个对象的时候, 我们必须要知道这个表达式的返回类型, 但是有的时候我们很难或者无法知道这个表达式或者函数的返回类型. 这个时候, 我们就可以使用auto关键字来让编译器帮助我们分析表达式或者函数所属的类型。
    【decltype的背景】:
    有时我们希望从表达式的类型推断出要定义的变量类型,但是不想用该表达式的值初始化变量(如果要初始化就用auto了)。为了满足这一需求,C++11新标准引入了decltype类型说明符,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
    【decltype的作用】:
    选择并返回操作数的数据类型。推断出表达式的表达式的类型,而不用表达式的值来初始化对象(即不实际计算表达式的值)。
    【auto/decltype的用法】:
    1、auto/decltype+引用
    auto 会忽略引用修饰
    int i = 0;
    int &a = i;
    auto b = a; //b 是int 类型,而不是引用类型。
    b = 10;
    cout << i << endle;// i的值为0
    分析:直观上感觉,a是i的引用,b自然也会推导出int&这个类型。但实际上,a只是i的别名,b类型的推导结果还是int。于是,对b的修改和i无关,结果输出0。
    推导出引用,需要加&:
    auto& b = a ;// 或者auto& b =i
    decltype(a) b; // int &
    ① 如果表达式是引用类型, 那么decltype的类型也是引用
    const int i = 3, &j = i;
    decltype(j) k = 5; // k的类型是 const int &
    ② 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
    int i = 3, &r = i;
    decltype(r + 0) t = 5; // 此时是int类型
    ③ 对指针的解引用操作返回的是引用类型
    int i = 3, j = 6, p = &i;
    decltype(
    p) c = j; // c是int类型的引用, c和j绑定在一起
    ④ 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
    int i = 3;
    decltype((i)) j = i; // 此时j的类型是int类型的引用, j和i绑定在了一起
    2、auto/decltype+const
    auto 会忽略顶层的const。如下:
    const int i =0;
    auto a = i; //a: int
    auto b = &i;
    // b : const int ,变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int
    推导出const,需要加const:
    const auto a = i; //a : const int
    decltype(i) a; //const int
    比较特别的是:如果decltype里面的表达式被包含在括号中,视为对表达式求值的类型。
    3、auto/decltype+指针
    auto 要声明为一个指针, 既可以加上也可以不加
    int i = 0;
    auto p1 = &i; //推断出int 类型, p1 是指向int 的指针。
    auto p2 = &i; // 推断出int
    类型,p2 是指向int的指针。
    【decltype和auto的差异】:
    decltype和auto都可以用来推断类型,但是二者有几处明显的差异:
    1.auto忽略顶层const,decltype保留顶层const;
    2.对引用操作,auto推断出原有类型,decltype推断出引用;
    3.对解引用操作,auto推断出原有类型,decltype推断出引用;
    4.auto推断时会实际执行,decltype不会执行,只做分析
    总之在使用中过程中和const、引用和指针结合时需要特别小心。

3 类型转换

dynamic_cast/static_cast/reinterpret_cast /const_cast
C++类型转换分为:隐式类型转换和显式类型转换

2.1 隐式类型转换

又称为“标准转换”,包括以下几种情况:
1) 算术转换(Arithmetic conversion) : 在混合类型的算术表达式中, 最宽的数据类型成为目标转换类型
int ival =3;
double dval =3.14159;
ival + dval;//ival被提升为double类型
2) 一种类型表达式赋值给另一种类型的对象:目标类型是被赋值对象的类型
intpi =0; // 0被转化为int 类型
ival = dval; // double->int
例外:void指针赋值给其他指定类型指针时,不存在标准转换,编译出错
3)将一个表达式作为实参传递给函数调用,此时形参和实参类型不一致:目标转换类型为形参的类型
extern double sqrt(double);
cout <<”The square root of 2 is “<< sqrt(2) << endl;
//2被提升为double类型:2.0
4)从一个函数返回一个表达式,表达式类型与返回类型不一致:目标转换类型为函数的返回类型
double difference(int ival1, int ival2)
{
return ival1 - ival2;
//返回值被提升为double类型
}
第2部分. 显式类型转换
被称为“强制类型转换”(cast)
C 风格: (type-id)
C++风格: static_cast、dynamic_cast、reinterpret_cast、和const_cast..
关于强制类型转换的问题,很多书都讨论过,写的最详细的是C++ 之父的《C++ 的设计和演化》。最好的解决方法就是不要使用C风格的强制类型转换,而是使用标准C++的类型转换符:static_cast, dynamic_cast。
标准C++中有四个类型转换符:static_cast、dynamic_cast、reinterpret_cast、和const_cast。下面对它们一一进行介绍。
static_cast
用法:static_cast < type-id > ( expression )
说明:该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
来源:为什么需要static_cast强制转换?
情况1:void指针->其他类型指针
情况2:改变通常的标准转换,同C风格类型转换。
情况3:避免出现可能多种转换的歧义
它主要有如下几种用途
1、用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的
2、用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
3、把void指针转换成目标类型的指针(不安全!!)
4、把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
dynamic_cast
用法:dynamic_cast < type-id > ( expression )
说明:该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
来源:为什么需要dynamic_cast强制转换?
基类中的虚函数无法满足要求,需要在子类中添加函数的时候。
例如:mediaplayer的基类继承问题。
dynamic_cast的用途:
dynamic_cast主要用于类层次间的上行转换下行转换,还可以用于*类之间的交叉转换

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

  1. class Base
  2. {
  3. public:
  4. int m_iNum;
  5. virtual void foo();
  6. };
  7. class Derived:public Base
  8. {
  9. public:
  10. char* m_szName[100];
  11. };
  12. void func(Base *pb)
  13. {
  14. Derived *pd1 = static_cast<Derived *>(pb);
  15. Derived *pd2 = dynamic_cast<Derived *>(pb);
  16. }

在上面的代码段中,
如果pb实际指向一个Derived类型的对象,pd1和pd2是一样的,并且对这两个指针执行Derived类型的任何操作都是安全的;
如果pb实际指向的是一个Base类型的对象,那么pd1将是一个指向该对象的指针,对它进行Derived类型的操作将是不安全的(如访问m_szName),访问结果和实际的不一致,而pd2将是一个空指针(即0,因为dynamic_cast失败)。
另外要注意:Base要有虚函数,否则会编译出错;static_cast则没有这个限制。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。
另外,dynamic_cast还支持交叉转换(cross cast)。如下代码所示。

  1. class Base
  2. {
  3. public:
  4. int m_iNum;
  5. virtual void f(){}
  6. };
  7. class Derived1 : public Base {};
  8. class Derived2 : public Base {};
  9. void foo()
  10. {
  11. derived1 *pd1 =new Drived1;
  12. pd1->m_iNum =100;
  13. Derived2 *pd2 = static_cast(pd1); //compile error
  14. Derived2 *pd2 = dynamic_cast(pd1); //pd2 is NULL
  15. delete pd1;
  16. }

在函数foo中,使用static_cast进行转换是不被允许的,将在编译时出错;而使用 dynamic_cast的转换则是允许的,结果是空指针。
reinterpret_cast
用法:reinterpret_cast < type-id > (expression)
说明:用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针
它可以把一个指针转换成一个整数,
也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。
const_cast
用法:const_cast< type-id > (expression)
说明:该运算符用来修改(去除)类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;
常量对象被转换成非常量对象。
Voiatile和const类试。举如下一例:
class B{
public:
int m_iNum;
}
void foo(){
const B b1;
b1.m_iNum = 100; //comile error
B b2 = const_cast(b1);
b2. m_iNum = 200; //fine
}
上面的代码编译时会报错,因为b1是一个常量对象,不能对它进行改变;使用const_cast把它转换成一个常量对象,就可以对它的数据成员任意改变。注意:b1和b2是两个不同的对象。
dynamic_cast和static_cast的区别:
dynamic_cast可以实现运行期类型安全检查(RTTI),是一种更加安全的方法,但是仅仅对多态类型有效,而且只能用于指针或者引用类型的转换上。
static_cast则可应用与任何类型,而且不需要类型实现了多态。static_cast的应用更加广泛,但是dynamic_cast更加强大和安全。
对象占用内存分析:
为什么通过一个实际指向了基类对象的子类指针调用子类的方法,既然没有出现错误并且可以顺利调用?
一个类无非就是包含两种成员:数据和方法(数据占用内存,方法不占内存)。那么当我们实例化出一个对象的时候,这个对象包含了哪些东西,实际占用的内存大小是多少?写一段代码试一试:
class Base { public: Base():m_b(4){}; int m_b; virtual void m_funcB(){cout << “base” << endl;}; }; class Derived:public Base { public: Derived():m_d(3){}; int m_d; void m_funcD(){cout << “derived” << endl;}; }; int main() { cout << sizeof(Base) << endl; cout << sizeof(Derived) << endl; }
打印出的结果分别是8和12。
那么一个类或者说对象占用的内存到底怎么计算呢?以Base为例,首先成员变量m_b占用了4个字节,其次,由于m_funcB是虚函数,因此要有一张虚函数表,其实就是一个指向表的指针,无论是什么类型的指针,占用的大小总是4字节,因此base占用了8个字节的大小。而Derived除了继承了Base的成员m_b之外,也保存了虚函数表的地址,还有自己的成员变量m_d,所以占用了12个字节。
或者有人会问:构造函数呢?还有虚函数本身不是还有函数体吗?难道不用计算进去?确实,类的函数是不会存储在实例化出来的对象里的,试想,对于每个对象,函数实现都是一样的,如果每实例化一个对象就存储一次函数体,不是毫无必要并且对内存使用而言是极大的浪费?
函数编译出来后是作为代码的一部分放在代码段中的,因此只要我们定义了Derived指针,无论这个实际指针指向什么对象,由于程序“事先”已经知道了这个方法属于哪个类,只要指针的类型正确,都可以正确找到调用函数的入口。所以即使我们的代码这么写,也是可以正确运行的:
void p2 = (int)0; Derived p3= (Derived)p2; cout << p3->m_funcD() << endl;
不管把什么地址赋给p2,都能正确地执行m_funcD函数。当然如果p3定义成其他类型,那么编译就会出错。
如果执行以下代码:
void p2 = (int)0; Derived p3= (Derived)p2; cout << p3->m_d << endl;
那么程序就会出现错误了,因为和成员函数不同,成员变量是每个对象都会在内存中用实际的内存地址存储,所以说成员函数属于类,成员变量属于各自的对象。

4 lambda表达式

C++ 语言中的lambda表达式在很多情况下提供了函数对象的另一种实现机制。Lambda表达式并不是STL所特有的,但它广泛应用于这一环境中。Lambda是表达式是定义一个没有名称、也不需要显示类定义的函数对象。Lambda表达式一般作为一种手段,用来将函数作为实参传递到另一个函数。相比于定义和创建一个常规的函数对象而言,lambda表达式非常容易使用和理解,而且需要的代码也较少。当然,一般而言,lambda表达式并不会取代函数对象。