首页 > 技术文章 > C++11笔记

ourroad 原文

__func__宏

__func__返回当前的函数名,也可以返回class和struct名。

/*返回函数名hello*/
const char* hello()
{
     return __func__;
}
/*返回结构体名foo*/
struct foo
{
     foo():name(__func){}
     const char* name;
}

__VA_ARGS__宏

可变参数宏

#define INFO(...) printf(__VA_ARGS)

noexcept

noexcept操作会阻止异常扩散,被noexcept修饰的函数,如果throw()抛出异常,则直接调用std::terminate()结束程序,catch并不能捕获到该异常。noexcept(false)表示始终抛出异常,noexcept相当于noexcept(true)操作。

花括号{}初始化

花括号初始化,统一了C++初始化方式,可以初始化类非静态成员变量、结构体、普通变量、对象等,类非静态变量初始化时,初始化列表后在就地初始化之后起作用。


struct foo

{

     int a;

     int b;

}f{1, 2};


class test

{

public:

     test(int i):a(i){}

private:

     int a{2};

     int b = 4;

     foo f{3,4};

};


int a{1};

test t{6};

sizeof运算符

sizeof运算符可以对类成员直接进行求值

class foo
{
public:
     int a;
     int b;
};

std::cout << sizeof(foo::a) << std::endl; //C++11以前不合法

final和override

final阻止派生类对基类函数的重载,override修饰的函数则要求该函数必须正确从基类虚函数继承。


class objcct
{
public:
     virtual void print() = 0;
};

class base : public object
{
public:
     void print() final;
};

class Derive : public base
{
public:
     //编译错误
     void print();
};
struct Base
{
     virtual void Turing() = 0;
     virtual void Dijkstra() = 0;
     virtual void VNeumann(int g) = 0;
     virtual void DKnuth() const;
     void Print();
};

struct DerivedMid : public Base
{
     void VNeumann(double g);
};

struct DerivedTop : public DerivedMid
{
     void Turing() override;
     void Dikjstra() override;           //无法通过编译,拼写错误,并非重载
     void VNeumann(double g) override;   //无法通过编译,参数不一致
     void DKnuth() override;             //无法通过编译,非常量重载
     void Print() override;              //无法通过编译,非虚函数重载
};

继承构造函数

多重继承中,如果基类构造函数参数很多,那么我们在派生类中构造函数初始化列表中初始化基类,需要书写大量的代码,继承构造函数,只需要进行声明即可将基类构造函数继承过来。派生类一旦继承了基类的构造函数,则派生类不会再自动生成默认构造函数。如果派生类从多个基类中继承构造函数发生冲突,则需要派生类显示的定义此构造函数。

struct A
{
     A(int i){}
     A(double d, int i) {}
     A(float f, int i, const char* c) {}
};

struct B : A {
     using A::A;
     int d{0};
};

int main(){
     B b(356); //b.i = 356
     B b1(2.3, 234); //b1.d = 2.3, b.i=234;
}

委托构造函数

委托构造函数主要解决,类有多个构造函数,构造函数参数个数不同时,需要书写很多的初始化代码,如下所示。委托构造函数要早于目标构造函数执行,委托构造函数和初始化列表不能同时出现,否则会出现冲突。同样一个目标构造函数既可以使用委托构造函数初始化,也可以作为其他构造函数的委托构造函数,从而形成一个委托链,但不能出现委托构造环。另外我们可以用try,catch捕获委托构造函数抛出的异常。


/*C++98写法*/
class Info{
public:
     Info() : type(1), name('a') { InitRest(); }
     Info(int i) : type(i), name('a') { InitRest(); }
     Info(char e) : type(1), name(e) { InitRest(); }
};

/*C++11写法*/
class Info{
public:
     Info() : type(1), name('a') { InitRest(); }
     Info(int i) : Info() { type = i; }
     Info(char e) : Info() { name = e; }
};

右值引用

左值一般指有名字的,可以取地址的值,反之无法取地址,没有名字的就是右值。例如:a = b + c; a就是左值,(b + c) 就是右值, 表达式(1 + 2)属于纯右值。左值引用T & a, 右值引用 T && a。引入右值主要为了引入移动构造函数,C++98中对类中具有指针类型变量时,一般需要重写拷贝构造函数和重载赋值操作,重新申请一块内存空间,将原来类指针内容拷贝进去,不理会原来的类是否就要释放。C++11引入移动构造函数,则可以从原来的类中将内存空间直接嫁接过来,减少内存的申请释放操作。


class foo
{
public:
     foo(const int a) : m_data(new int(a)){}
     //拷贝构造函数
     foo(const foo& s) : m_data(new int(0))
     {
          if (s.m_data != nullptr)
               *m_data = *(s.m_data);
     }
     
     //移动构造函数
     foo(const foo && s) : m_data(s.m_data)
     {
          m_data = nullptr;
     }

private:
     int* m_data;
};

  上面代码中foo类是典型的类内包含指针类型的成员函数,C++98中需要实现拷贝构造函数,分配内存,然后copy内存数据。C++11可以实现移动构造函数,移动构造函数的参数是类的右值类型,因此构造类对象时,传入的参数是右值,则使用移动构造函数构造对象。上面代码中如果没有实现移动构造函数,传入右值进行构造时,则调用拷贝构造函数。实现类时需要自己考虑是否需要实现移动构造函数,构造新对象时需要考虑是否需要通过移动构造函数构造。移动构造函数最好使用noexcept修饰,保证不会抛出异常,抛出异常非常危险,很可能导致内存泄露。
  C++中提供了将左值转换为右值引用的函数std::move(左值),函数返回的是右值引用,如下代码。模板编程时由于使用typedef重定义类型,会出现多个引用符号,如下代码,C++11中规定T和TR中只要出现左值引用,则定义v按照左值引用处理,下面代码中v和v1均为左值引用。模板编程时需要对重载函数调用时,需要考虑不同参数类型如何进行传参问题,C++11提供了std::forward函数进行完美转发,无需考虑引用叠加问题,直接进行参数传递。


foo f(3);
foo t(std::move(f)); //std::move()函数将左值f转换为右值,触发foo调用移动构造函数构造t对象。

//引用叠加
typedef T& TR;
TR& v;
TR&& v1;

void DoSomething(int& a){std::cout << "int&" << std::endl; }
void DoSomething(int&& a){std::cout << "int&&" << std::endl; }
void DoSomething(const int & a){std::cout << "const int&" << std::endl; }
void DoSomething(const int && a){std::cout << "const int&&" << std::endl;}

template<typename T>
void PerfectForward(T && t)
{
     DoSomething(std::forward<T>(t));
}

显式转换操作符

C++98中可以显示转换内置类型,比如bool,char*等,但是不允许显示转换自定义类型,C++11中添加了对自定义显式转换操作符支持。explicit关键字C++98中只能修饰构造函数,令对象只能进行显式的构造,C++11对其进行了扩展,可以修饰自定义转换操作符函数,令对象只能显式进行转换。


class foo
{
public:

     operator bool () const
     {
          return true;
     }
     
     //xxx为自定义类型,可以是我们自定义类,自定结构体
     operator xxx ()
     {
     }
     
     //explicit关键字修饰,则foo转换为yyy只能通过显式的转换,即使用static_cast<yyy>()
     explicit operator yyy ()
     {
     }
};

初始化列表

C++11对初始化方式进行了扩展,使用大括号可以初始化list, vector, map等数据类型,并且支持自定义类型的初始化。


std::vector<int> v = {1, 2, 3};
std::map<int, int> m = {{1, 2}, {3, 4}, {5, 6}};

模板别名

C++对using进行了扩展可以使用using给模板定义别名,达到比typedef更加强大的功能。


template<typename K, typename V>
using IntStrMap = std::map<K, V>;

IntStrMap<int, std::string> m;

SFINEA规则

SFINEA即匹配失败不是错误,下面例子C++98中标准会报编译错误,C++11对进行了扩展,允许编译通过。

struct Test {
     typedef int foo;
};

template <typename T>
void f(typename T::foo) {}

template <typename T>
void f(T) {}

int main(){
     f<Test>(10);
     f<int>(10);
}

auto关键字

auto在C++11中被赋予了新的含义,自动推导变量类型,慎用,如果大量auto出现,势必会降低程序的可阅读性,auto并非万能,下面列出不能推导的情形。

1.不能作为函数形参类型推导。 void fun(auto i = 2);
2.结构体中非静态成员不能推导。 struct { auto i = 1; }
3.不能推导数组类型。 auto a[3] = {0};
4.不能作为模板实例化的参数。std::vector v = {1};

decltype关键字

decltype也是C++11中进行类型推导的关键字,auto类似于动态语言中的var,decltype则是从变量或返回值中推导类型。

int i = 0;
decltype(i) j = 1;

float foo() {}
decltype(foo()) a = 1.0;

decltype可以作为函数返回值推导,模板中大有用处。

auto foo(int i) -> decltype(i) { return i; }

auto会继承原来变量的const和volatile属性,decltype推导时不会继承原来的变量的const和volatile

for循环

std::vector<int> vec = {0, 1, 2};
for (auto i : vec)
     std::cout << i << std::endl;

强枚举类型

C++98中枚举类型作用域是全局的,并且底层长度不定,可以隐式的进行类型转换。C++11中定义了一种强类型枚举。

1.强作用域,强类型枚举成员的名称不会被输出到其父作用域空间。
2.转换限制,强类型枚举成员的值不可以与整型隐式地相互转换。
3.可以指定底层类型,强类型枚举默认的底层类型为int,但是可以显式地指定底层类型,底层类型不可以是wchar_t类型。

enum class Type:int { General, Light, Medium, Heavy};
Type t = Type::Light;

智能指针

C++11废弃了auto_ptr指针,重新实现了三种智能指针,其实boost中很早就实现了,用法和原理差不多。

1.unique_ptr 指针只能有一个对象拥有,不可以共享,不可以复制,move操作后,原来对象指针将为空,先的对象将唯一拥有该指针。

unique_ptr<int> up1(new int(11));
unique_ptr<int> up2 = up1; //错误无法复制
unique_ptr<int> up3 = move(up1); //move操作后up3将为空,*up1将导致运行错误

2.shared_ptr 指针可以被多个对象公用,通过引用计数方式标记,计数为0时,删除指针指向内存。
3.weak_ptr 可以指向shared_ptr对象,但是weak_ptr操作不会影响shared_ptr的计数,通常用于判断shared_ptr是否有效。lock函数可以返回一个shared_ptr对象,但是不会增加计数

shared_ptr<int> sp1(new int(22));
weak_ptr<int> wp = sp;
shared_ptr<int> sp2 = wp.lock(); //并不会增加引用计数值
sp.reset();  //释放智能指针指向内存。

constexpr

constexpr 修饰的变量会被编译为常量,前提是它是常量,可以定义常量表达式函数,常量表达式编译器可以进行运算,感觉老式const只是定义常量不能进行修改,但是并不能告诉编译器编译器参与计算。constexpr却可以作为数组初始化的长度,枚举类型初始化,switch-case的case表达式通过编译,还可以修饰构造函数,从而定义一个constexpr修饰的类对象。

constexpr修饰常量表达式函数必须满足下面条件

1.函数体只有单一的return 返回语句。
2.函数必须有返回值(不能时void函数)。
3.函数使用前必须已有定义,不能只进行声明。
4.return返回语句表达式中不能使用非常量表达式的函数,全局数据,且必须是一个常量表达式,也就是调用的整个链都必须用constexpr修饰。

变长模板参数

C++98如果模板中需要传递多个参数都是通过宏或者直接手写一些特化版本,boost中bind和function实现均是如此,C++11中则提供了变长模板参数。


template<typename ...T> class TempArgs {};
template<typename Head, typename ...Tail>
class TempArgs<Head, Tail...>: private TempArgs<Tail...>
{
      Head head;  
};

template<>
class TempArgs<>{};

//变参模板函数
void print() { std::cout << "end" << std::endl; }
template<typename T1, typename ...T>
void print(T1 value, T... args)
{
    std::cout << value << std::endl;
    print(args...);
}

线程相关

C++11中引入了线程库,原子操作库,线程本地存储等,其中原子库std::atomic,首次明确了内存模型,进行性能优化时,可以考虑使用。线程本地存储使用thread_local修饰保证线程本地变量。

指针空值nullptr

nullptr特指指针空值,彻底和老式的NULL其实就是0决裂,可以直接使用nullptr给指针类型置空,nullptr和NULL之间不能直接转换,nullptr是nullptr_t类型,所有的nullptr_t类型都是一样的。

匿名函数

lambda函数即匿名函数,C++11中一大亮点,以前使用STL中的算法时,需要定义一个仿函数或者是一个全局函数,极不方便,有了匿名函数可以直接在调用地方书写函数,方便又清晰。[]中是引用列表,“=”表示值传递父作用域变量,“&”表示引用传递父作用域,默认lambda是const类型,不允许修改传入参数值。

auto fn1 = [=](int a) -> int{ return a; };
auto fn2 = [&]() -> int{ return 2; };

std::cout << fn1(1) << std::endl;
std::cout << fn2() << std::endl;

对齐方式

C++98中并没有明确对齐方式,所有的对齐都是编译器隐式完成,也可以使用编译提供的特性,指定对齐方式,例如 #pragma pack(n),特别是struct类型在跨网络传输时要特别注意对齐方式,C++11中使用alignas关键字指定对齐方式,alignof计算变量对齐方式。

struct alignas(int) foo
{
     char c;
     int a;
};

std::cout << alignof(foo) << std::endl;

Unicode支持

C++定义原生支持Unicode编码,char16_t用于存储UTF-16编码,char32_t用于存储UTF-32编码;前缀表示u8表示UTF-8编码,u表示UTF-16编码,U表示为UTF-32编码。R前缀支持原生字符串输出,并不会进行转义,u8R,uR,UR分别支持UTF-8,UTF-16,UTF-32原生字符串。

推荐阅读