初始化列表
C++11 添加了 initializer_list 类型,允许使用 initializer_list 初始化对象
STL 容器例如 map, vector 等都实现了 initalizer_list constructor
自定义的对象也可以使用 initalizer_list 作为构造函数的参数
class CVector
{
private:
std::vector<int> m_vec;
public:
CVector() {}
CVector(const std::initializer_list<int>& list)
{
std::cout << "CVector Constructor" << std::endl;
m_vec = list;
}
CVector& operator=(std::initializer_list<int>& list)
{
std::cout << "Assignment function" << std::endl;
m_vec = list;
return *this;
}
friend std::ostream& operator<<(std::ostream& os, CVector& myVec);
};
std::ostream& operator<<(std::ostream& os, CVector& myVec)
{
for (auto iter = myVec.m_vec.begin(); iter != myVec.m_vec.end(); iter++)
{
os << *iter << " ";
}
os << std::endl;
return os;
}
int main()
{
std::vector<int> vec = {1, 2, 3};
//构造函数
CVector myVec{ 2, 3, 4 };
for (const auto& item : vec)
{
std::cout << item << " ";
}
std::cout << std::endl;
std::cout << myVec;
//构造函数
myVec = { 5, 6, 7 };
std::cout << myVec;
//赋值运算符
auto list = { 7, 8, 9 };
myVec = list;
std::cout << myVec;
return 0;
}
输出
1>CVector Constructor
1>1 2 3
1>2 3 4
1>CVector Constructor
1>5 6 7
1>Assignment function
1>7 8 9
std::initializer_list,在C++的stl容器中,可以进行任意长度的数据的初始化,使用初始化列表页只能进行固定参数的初始化,如果想要做到和stl一样有任意长度初始化的能力,可以使用std::initializer_list这个轻量级的模板类来实现。
先来介绍一下这个类模板的一些特点
它是一个轻量级的容器类型,内部定义了迭代器 iterator 等容器必须的概念,遍历时得到的迭代器是只读的。
对于 std::initializer_list 而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型 T
在 std::initializer_list 内部有三个成员接口:size(), begin(), end()。
std::initializer_list 对象只能被整体初始化或者赋值。
- 作为普通函数参数
void fun(std::initializer_list<int> list)
{
for (auto iter = list.begin(); iter != list.end(); iter++)
{
std::cout << *iter << std::endl;
}
}
int main()
{
fun({1, 2, 3, 4});
return 0;
}
- 作为构造函数参数
统一初始化
C++03 中,可以使用集合初始化也叫聚合类型的初始化
满足以下条件的类(class、struct、union)可以被看做是一个聚合类型:
无用户自定义的构造函数。
无私有或保护的非静态数据成员。
struct A{
int x;
float y;
};
A a = {1, 2.0};
C++11 允许通过同样的方式调用构造函数
struct A
{
int x;
float y;
friend std::ostream& operator<<(std::ostream& os, A& a);
};
std::ostream& operator<<(std::ostream& os, A& a)
{
std::cout << a.x << " " << a.y << std::endl;
return os;
}
struct B
{
int m_nX;
float m_nY;
B(int x, float y)
{
std::cout << "B Constructor"<< std::endl;
m_nX = x;
m_nY = y;
}
friend std::ostream& operator<<(std::ostream& os, B& b);
};
std::ostream& operator<<(std::ostream& os, B& b)
{
std::cout << b.m_nX << " " << b.m_nY << std::endl;
return os;
}
int main()
{
//不会初始化
A a;
std::cout << a;
//集合初始化
A aa = { 1, 2.0 };
std::cout << aa;
//全置为0
A aaa = { 0 };
std::cout << aaa;
//C++11 调用构造函数
B b = { 1, 2.0 };
std::cout << b;
}
输出
1>-858993460 -1.07374e+08
1>1 2
1>0 0
1>B Constructor
1>1 2
连同初始化列表,统称为统一初始化
统一初始化调用顺序优先级:
- initalizer_list constructor
- 构造函数
- 集合初始化
auto
非常实用的功能, 从复制操作的右值直接推断变量类型
auto并不代表一种实际的数据类型,只是一个类型的“占位符”,auto并不是万能的在任意场景下都能够推导出变量的实际类型,是用auto声明的变量必须进行初始化,已让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型
当变量不是指针或者引用类型时,推导的结果不会保留const,volatile关键字
当变量是指针或者引用类型时,推导的结果会保留const,volatile关键字
auto a = 1;
auto b = 2.0;
auto* p = "123"; //p 的类型是 const char* 因为变量是指针类型推导的结果会保留const
const int a = 10;
auto& i = a; //i 的类型是 const int& 因为变量是引用类型推导的结果会保留const
在 C++03 中,一个痛点:要迭代一个容器,需要
for(std::set<int>::iterator it = map.begin(); it != map.end(); ++it) {
/* Do something */
}
在 C++11 中,可以直接使用 auto
for(auto it = map.begin(); it != map.end(); ++it) {
/* Do something */
}
不能使用auto的场景
不能作为函数的形参使用,因为只有函数调用的时候才会给函数参数初始化
不能用于类的非静态成员变量初始化,类只有实例化时才会给成员变量初始化
class Test
{
auto v1 = 0; // error 实例化成对象才会给它赋值
static auto v2 = 0; // error 类的静态非常量成员不允许在类内部直接初始化
static const auto v3 = 0; //ok
};
auto无法定义数组
int arr[] = { 1, 2, 3, 4, 5 };
auto v1 = arr; //ok int*类型
auto v2[] = arr; //无法定义数组
auto v3[] = { 1, 2, 3, 4, 5 }; //无法定义数组
auto v4 = { 1, 2, 3 }; //std::initializer_list 类型
decltype
decltype是 “declare type”的缩写,意思是声明类型。decltype的推导实在编译期完成的,它只是用哦关于表达式类型的推导,并不会计算表达式的值。
int a = 10;
decltype(a) b = 99; // int类型
decltype(a + 3.14) c = 5; // double类型
decltype(a + b * c) d = 520; // double类型
推导规则
- 表达式为普通变量或者普通表达式或者类表达式,在这种情况下,是用decltype推导出的类型和表达式的类型是一致的。
class Test
{
public:
static const int value;
std::string text;
};
int main()
{
int x = 99;
const int& y = x;
decltype(x) a = x; //int
decltype(y) b = x; //const int&
decltype(Test::value) c = 0; //const int
Test t;
decltype(t.text) d = "124"; //std::string
}
- 表达式是函数调用,是用decltype推导出的类型和函数返回值一致。
int fun_int();
int& fun_int_r();
int&& fun_int_rr();
const int fun_cint();
const int& fun_cint_r();
const int&& fun_cint_rr();
int main()
{
int n = 0;
decltype(fun_int()) a = 0; //int
decltype(fun_int_r()) b = n; //int&
decltype(fun_int_rr()) c = 0; //int&&
decltype(fun_cint()) d = 0; //const int
decltype(fun_cint_r()) e = n; //const int&
decltype(fun_cint_rr()) f = 0; //const int&&
}
- 表达式是一个左值(可以取地址的变量),或者被括号()包围,使用decltype推导出的是表达式类型的引用(如果有const,volatle限定符不能忽略)
class Test
{
public:
int num;
};
int main()
{
const Test obj;
//普通变量
decltype(obj.num) a = 0; //int
//被括号()包围
decltype((obj.num)) b = 0; //const int&
int n = 0, m = 0;
//普通表达式
decltype(n + m) c = 0; //int
//表达式是一个右值
decltype(n = n + m) d = n; //int&
}
decltype在泛型编程中的应用
//遍历并打印容器里面的值
template <typename T>
class Container
{
public:
void print(T& t)
{
for (m_it = t.begin(); m_it != t.end(); m_it++)
{
std::cout << *m_it << std::endl;
}
}
private:
//推导出迭代器类型
decltype(T().begin()) m_it;
};
int main()
{
std::list<int> ls{ 1, 2, 3, 4 };
Container<std::list<int>> c;
c.print(ls);
const std::list<int> ls1{ 7, 8, 9 };
Container<const std::list<int>> c1;
c1.print(ls1);
}
返回值类型后置
语法
auto fun(参数1, 参数2....) -> decltype(t + u)
实例
//根据入参推导返回值类型
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
int main()
{
//返回int
std::cout << add<int, int>(1, 2) << std::endl;
std::cout << add(1, 2) << std::endl;
//返回double
std::cout << add(1, 3.14) << std::endl;
}
基于范围的for循环
对于任何一个带有 begin() 和 end() 方法的对象,例如 vector
vector<int> v = {1, 2, 3};
for (int i: v) {cout << i << endl;}
提示:配合 auto 使用效果更佳
int main(void)
{
map<int, string> m{
{1, "lucy"},{2, "lily"},{3, "tom"}
};
for (auto& item : m)
{
// item.first 是一个常量
cout << "id: " << item.first++ << ", name: " << item.second << endl; // error
}
return 0;
}
nullptr
在 C++03 中, 使用 NULL 代表空指针, 但 NULL 被定义为 0, 有时候会有歧义
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
在C++中NULL 为0,C语言中NULL为((void )0),因为C++不允许隐式的将void转为其他类型的指针比如
void* pVoid = 0;
int* p = pVoid; //编译器报错
在 C++11 中, 使用专门的 nullptr 代表空指针, 用以解决以下问题
void func(int i) {}
void func(char* p) {}
int main() {
// 有歧义, 编译器报错
func(NULL);
// C++11, 调用 func(char* p)
func(nullptr);
}
强类型枚举
C++03 中,枚举常量被暴露在同一作用域中,且被隐式转化为整形
C++11 引入了 enum class 解决这个问题
enum Color { red, blue };
enum Animal { dog, cat };
enum class Person
{
man = 20, woman = 10
};
enum class KPerson
{
man = 20, woman = 10
};
int main()
{
if (red == dog)
{
std::cout << "true" << std::endl;
}
if (Color::red == Animal::dog)
{
std::cout << "true" << std::endl;;
}
//编译器报错
if (Person::man == KPerson::man)
{
std::cout << "true";
}
//允许
if (Person::man == Person::man)
{
std::cout << "true";
}
}
静态断言
C++11 允许使用 static_assert 在编译期执行断言
int main()
{
static_assert(sizeof(int) == 4, "sizeof(int) error");
std::cout << "Hello World!\n";
}
委托构造函数
在 C++03 中,如果两个构造函数都需要执行相同的操作,只能
class A {
init() {/*Do something*/}
public:
A() {init();}
A(int a) {
init();
/* Do something with a */
}
};
额外定义的 init() 函数可能被误调用
C++11 则允许在构造函数开头调用另一个构造函数, 也允许在声明时初始化成员变量
class A
{
public:
A()
{
m_str = "Hello World";
std::cout << "A()" << std::endl;
}
A(int nNum) : A() //代理构造函数
{
m_nNum = nNum;
std::cout << "A(int nNum)" << std::endl;
}
private:
int m_nNum = 10; //调用任意构造函数都会初始化 m_nNum = 10;
std::string m_str;
};
int main()
{
A a;
A b(20);
}
Override
C++11 引入了 override 关键字,避免继承时错误的创造新函数
class A {
virtual void a(int);
};
class AA: public A {
// 创建一个新函数
virtual void a(float);
// 编译期报错
virtual void a(float) override;
};
final
C++11 引入 final 关键字,用于修饰类和虚函数,表示不允许某个类被继承/或者某个虚函数补鞥呢被重写
class Dog final { // Dog 不可继承
...
};
class Dog {
// bark() 不可重载
virtual void bark() final;
};
默认构造函数
C++11 允许使用 default 关键字,由编译器自动生成默认构造函数
// C++03
class Dog {
public:
// 不会生成默认构造函数
dog(int age) {}
};
Dog dog; // 编译器报错
// C++11
class Dog {
public:
dog(int age) {}
// 编译器自动生成默认构造函数
dog() = default;
};
被删除的函数
C++03 中, delete 关键字仅用于释放动态分配的内存
C++11 允许使用 =delete 将函数标记为被删除的, 可以用来阻止隐式类型转换等
class Dog {
public:
dog(int age) {}
dog(double) = delete;
};
Dog(1);
Dog(2.0); // 编译器报错
constexpr
C++11 中, constexpr 扩展了原有 const 的用法, 允许在编译期对作用于常量的函数求值
const并未区分出编译期常量和运行期常量
constexpr限定在了编译期常量
int a = 1; //ok
const int i = 100; //ok
constexpr int j = 200; //ok
constexpr int k = j + 200; //ok
const int b = a + 10; //ok 运行时常量
constexpr int z = a + 10; //error
constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。而检测constexpr函数是否产生编译时期值的方法很简单,就是利用std::array需要编译期常值才能编译通过的小技巧。
常量表达式函数(普通函数/类成员函数,类的构造函数,模板函数),函数必须要有返回值,并且return返回的表达式必须是常量表达式,void不行,函数在是用之前,必须有对应的定义不能只声明,整个函数的函数体内,不能出现非常量之外的语句。
constexpr int foo(int i)
{
return i + 5; //通过常量计算得到常量
}
int main()
{
int i = 10;
std::array<int, foo(5)> arr; // OK
foo(i); // Call is Ok
// But...
std::array<int, foo(i)> arr1; // Error
}
类成员函数
class Test
{
public:
constexpr int fun1()
{
constexpr int var = 100;
return var - 50;
}
};
int main()
{
Test test;
constexpr int num = test.fun1();
std::cout << num << std::endl;
}
模板函数
struct Person
{
const char* name;
int age;
};
//定义函数模板
template<typename T>
constexpr T display(T t)
{
return t;
}
int main()
{
struct Person p {"yuan", 19 };
//普通函数
struct Person ret = display(p);
//常量表达式函数
constexpr int ret1 = display(50);
}
类的构造函数
构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值
struct Person
{
constexpr Person(const char* p, int age) : name(p), age(age)
{
}
private:
const char* name;
int age;
};
字符串字面量
C++11 引入了更多的字面量定义方式,方便定义 unicode 字符串以及正则表达式等
const char *a = u8"你好"; // UTF-8
const char16_t *b = u"你好"; // UTF-16
const char32_t *c = U"你好"; // UTF-32
定义原始字符串的字面量,定义方式为: R"xxx(原始字符串)xxx" 其中()两边的字符串可以省略。左右的xxx必须相同是对字符串的描述相当于注释实际不属于字符串的内容,原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做转义或连接等操作
//原始字符串,直接赋值
std::string str = "E:\Program Files (x86)\360\360zip";
std::cout << str << std::endl;
//对需要转义的字符串进行转义
str = "E:\\Program Files (x86)\\360\\360zip";
std::cout << str << std::endl;
//C++11 写法
str = R"(E:\Program Files (x86)\360\360zip)";
std::cout << str << std::endl;
//转义双引号,为了可读性换行每行后面加(\)
str = "{\
\"status\":200,\
\"data\":[],\
\"message\":null\
}";
std::cout << str << std::endl;
//非常方便
str = R"({
"status":200,
"data":[],
"message":null
})";
std::cout << str << std::endl;
输出
1>E:Program Files (x86)痧zip
1>E:\Program Files (x86)\360\360zip
1>E:\Program Files (x86)\360\360zip
1>{ “status”:200, “data”:[], “message”:null }
1>{
1> “status”:200,
1> “data”:[],
1> “message”:null
1> }
对模板右尖括号的优化
在泛型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(>>)会被编译器解析成右移操作符,而不是模板参数表的结束。
C++11改进了编译器的解析规则,尽可能的将多个右尖括号 >> 解析成模板参数结束符,方便我们编写模板相关的代码。
Base<vector> b
这句代码在支持C++11的编译器中编译是没有问题的,如果使用g++直接编译需要加参数-std=c++11
g++ test.cpp -std=c++11 -o app
如果不想让编译器支持c++11
g++ test.cpp -std=c++03 -o app
函数模板的默认模板参数
在 C++98/03 标准中,类模板可以有默认的模板参数
但是不支持函数的默认模板参数,在C++11中添加了对函数模板默认参数的支持
template <typename T = long, typename U = int>
void myTest(T t = 'A', U u = 'B')
{
std::cout << "t: " << t << " u: " << u << std::endl;
}
int main()
{
//自动推导,根据传递的实参
//myTest<char, char>
myTest('a', 'b');
//myTest<int, char> 第一个使用指定的int 第二个根据传递的实参推导
myTest<int>('a', 'b');
//myTest<char, char> 第一个使用指定的char 第二个根据传递的实参推导
myTest<char>('a', 'b');
//myTest<int, char> 第一个和第二个都为指定的类型
myTest<int, char>('a', 'b');
//myTest<long, int> 使用默认的模板类型 和默认的参数
myTest();
return 0;
}
当默认模板参数和模板参数自动推导同时使用时(优先级从高到低)
- 如果可以推导出参数类型则使用推导出的类型
- 如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数
- 如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错。
using
在 C++ 中 using 用于声明命名空间,使用命名空间也可以防止命名冲突。在程序中声明了命名空间之后,就可以直接使用命名空间中的定义的类了,在C++11中赋予了using新的额功能。
- 定义别名
在 C++ 中可以通过 typedef 重定义一个类型,语法格式如下:
typedef 旧的类型名 新的类型名;
// 使用举例
typedef unsigned int uint_t;
使用 using 定义别名的语法格式是这样的:
using 新的类型 = 旧的类型;
// 使用举例
using uint_t = int;
using ui = unsigned int;
ui a = 1;
通过 using 和 typedef 的语法格式可以看到二者的使用没有太大的区别,假设我们定义一个函数指针,using 的优势就能凸显出来了,看一下下面的例子:
// 使用typedef定义函数指针
typedef int(*func_ptr)(int, double);
// 使用using定义函数指针
using func_ptr1 = int(*)(int, double);
- 模板的别名
使用 typedef 重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,比如我们需要一个固定以 int 类型为 key 的 map,它可以和很多类型的 value 值进行映射,如果使用 typedef 这样直接定义就非常麻烦:
typedef map<int, string> m1;
typedef map<int, int> m2;
typedef map<int, double> m3;
在这种情况下我们就不自觉的想到了模板:
template <typename T>
typedef map<int, T> type; // error, 语法错误
使用 using 来为一个模板定义别名
template <typename T>
using mymap = map<int, T>;
mymap<string> m;
m.insert(make_pair(1, "luffy"));
m.insert(make_pair(2, "ace"));
委托构造函数
委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化。
class Test
{
public:
Test()
{
std::cout << "Test()" << std::endl;
}
Test(int a):Test()
{
std::cout << "Test(a)" << std::endl;
}
Test(int a, int b):Test(a)
{
std::cout << "Test(a, b)" << std::endl;
}
};
int main()
{
Test t1;
Test t2(1);
Test t3(1, 2);
return 0;
}
继承构造函数
C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。
继承构造函数的使用方法是这样的:通过使用 using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。
另外如果在子类中隐藏了父类中的同名函数,也可以通过 using 的方式在子类中使用基类中的这些父类函数
class A
{
public:
A(int a) : m_a(a) {}
A(int a, int b) : A(a)
{
m_b = b;
}
A(int a, int b, int c) : A(a, b)
{
m_c = c;
}
void print()
{
std::cout << "A::print" << std::endl;
}
private:
int m_a;
int m_b;
int m_c;
};
class B : public A
{
public:
//继承构造函数
using A::A;
void print(int a)
{
std::cout << "B::print " << a << std::endl;
}
void print(int a, int b)
{
std::cout << "B::print " << a << " " << b << std::endl;
}
//子类中隐藏了父类中的同名函数,也可以通过 using 的方式在子类中使用基类中的这些父类函数
using A::print;
};
int main()
{
B b(1);
b.print();
b.print(1);
b.print(1, 2);
return 0;
}