深刻理解C++标准

前言

C++语言的标准演化与C语言不同,C语言的标准再怎么变化,始终和旧版本没有太大差别。而C++标准的迭代,感觉好像换了一种语言似的。

C++历史发展

C++语言发展大概可以分为三个阶段:

传统类型上的面向对象:封装、继承、多态三大特性突出。

泛型程序设计:因标准模板库(STL)和Boost库出现,泛型程序设计占了比重增大。

产生式编程和模板元编程:Boost以及其他库大量使用产生式编程和模板元编程,使其变得更加复杂。

经历到现在,C++已经是现代最复杂的编程语言。

C++标准

C++98 和 C++03

因为C++03与C++98差距并不大,C++03只是修复了C++98出现的问题,所以放在一块来写。

有个漏洞比较特殊,C++03为了解决这个漏洞,特意要求:std::vector 中的元素必须连续存储。

C++早期的时候,编译的时候会把C++转成C语言然后再转换成二进制,现在这种做法已经被淘汰了。

  1. 名称空间

名称空间可以有效的避免类名重复,比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace myspace{
class Buffer
{
public:
char* toChar();
}
}
namespace myBuffer{
class Buffer
{
public:
char* toChar();
}
}
int main()
{
//调用第一个命名空间的类对象
myspace::Buffer b1;
b1.toChar();
//调用第二个命名空间的类对象
myBuffer::Buffer b2;
b2.toChar();
return 0;
}
//平时如果我们不用第三方库,那么遇到重名的可以改下名字就可以了
//但是如果用到多个第三方库,那么重名的概率会大大增加,这个时候就要依赖命名空间去排除重名
//当然,如果你确保不会重名,可以使用using namespace +命名空间名称就可以不用指定调用哪个命名空间
//比如我们经常调用cout,cout属于std命名空间里面,我们可以使用using namespace std,来简化每次调用cout
  1. 强制类型转换

C++引入了四个强制类型转换运算符:

static_cast<> 将值从一种数值类型转换为另一种数值类型

  1. 分配内存

C++中使用new 和delete 来分配内存,替代了C语言中的malloc和free。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//new 和malloc不同,malloc必须指定要分配多少内存,new 则可以根据数据类型自动分配内存
//除此之外,如果是类对象,new还会尝试调用构造函数
int* pint = new int;
int* pints = (int*)malloc(sizeof(int)*1);

//free 和delete 有点相似,但delete 对象时会尝试调用对象的析构函数
free(pint);
delete pints; // 尝试调用析构函数

//free和delete为什么不需要知道内存大小就可以释放内存呢?
//在分配内存时,操作系统会给你分配多余的内存,这些多余的内存就会向护城河一样,把你要操作的内存区域围起来
//比如 fdfd .........fdfd, fdfd为护城河,.代表你使用的内存。

//分配数组,和释放数组
int* pz = new int[10];
delete [] pz;

C、C++这种手动分配内存,手动释放非常方便,能更好的利用内存去读取,写入大文件。

  1. vector 模板类

vector 是一种动态数组,它内部是使用new和delete 管理动态数组的。

1
2
3
4
5
6
7
#include <vector>
using namespace std;
int main()
{
vector<int> vi;
vi.push_back(10);
}
  1. 内联函数

普通函数执行的时候,会跳转到相对应的函数地址进行执行。

内联函数则是编译器将函数里面的内容拷贝出来,放在调用的地址,那么就不会有函数进栈出栈的开销了。

当然内联函数占用内存比普通函数要多,速度比普通函数稍微快一点。

1
2
3
4
5
6
7
8
9
10
inline double square(double x ){return x*x }
int main()
{
double a,b;
a = square(5.0);
b = square(4.5+7.5);
cout<<a<<endl;
cout<<b<<endl;
return 0;
}
  1. 引用变量

引用变量是一种复合类型,是已定义的变量的别名(可以理解成小名)。

在C++11中,把这种引用变量叫做左值引用。

1
2
3
4
5
6
7
int rats = 10;
int& b = rats;// &为引用符号,和char* 中的* 一样
//& 在变量名类型后和在变量名前,意义不一样。
//在类型后,代表引用类型
//在变量名前,代表取变量的地址
//在以前的C++版本中,可以引用表达式,但是现在不行。比如 sq(5+10),void sq(int& a)。
//假如不修改引用参数,应尽可能将引用形参声明为const
  1. 左值和右值

在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。

现在常规变量和const变量都可以视为左值,因为可以通过地址访问它们。

  1. 函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
void Swap(T &a,T & b)
{
T temp;
temp = a;
a = b;
b = temp;
}
//也可以这样定义函数模板
template <class T>
void Swap(T &a,T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
  1. 非模板函数、模板函数和显式具体化模板函数

非模板函数:即普通的函数

模板函数:即8所描述的函数模板

显式具体化模板函数:原型和定义应以template<>大头,并通过名称来指出类型

具体化优先于模板函数,而非模板函数优先于具体化和常规模板

1
2
3
4
//非函数模板和模板函数不用说了,主要看看显式具体化模板函数
//显式具体化函数,以前学的时候没有听过,看了C++ Primer才知道有这个
template<>void Swap<job> (job& ,job&);
//感觉基本用不上
  1. 类模板

类模板一般放在头文件里面,不能将模板成员函数放在独立的实现文件中。

由于模板不是函数,它们不能单独编译,必须与特定的模板实例化请求一起使用。

使用的时候,引入头文件,定义变量使用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#ifndef STACKTP_H
#define STACKTP_H
template <class TYPE>
class Stack
{
private:
enum{MAX = 10};
TYPE items[MAX];
int top;
public:
Stack();
bool isempty();
bool isfull();
bool push(const TYPE& item);
bool pop(TYPE& item);
};

template <class TYPE>
Stack<TYPE>::Stack()
{
top = 0;
}
template <class TYPE>
bool Stack<TYPE>::isempty()
{
return top == 0;
}

template <class TYPE>
bool Stack<TYPE>::isfull()
{
return top == MAX;
}
template <class TYPE>
bool Stack<TYPE>::push(const TYPE& item)
{
if(top < MAX)
{
items[top++] = item;
return true;
}else{
return false;
}
}
template <class TYPE>
bool Stack<TYPE>::pop(TYPE& item)
{
if(top > 0)
{
item = items[--top];
return true;
}else{
return false;
}
}

#endif


int main()
{
Stack<int> kernels;//仅在实例化的时候,才会生成模板类
}

C++ 11

  1. 初始化方式

C++11可以大括号初始化任何类型,可以使用=号,也可以不用。

字符串类型会被初始化为空字符串,数字类型会被初始化为0。复杂类型需要指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
int rocs = {}; //设置变量为0
int psychics{};//设置变量为0
int a[10]{};
for (int i = 0; i < 10; i++)
{
cout << a[i] << endl;
}
string b[10]{};
for (int i = 0; i < 10; i++)
{
cout << b[i] << endl;
}
return 0;
  1. auto声明

auto可以让编译器能够根据初始值的类型推断变量的类型,以前的auto是C语言的关键字,很少使用。

auto主要目的是为了减少些一大串的类型名,比如vector<vector<vector>>,非常适用于复杂而又长的类型名,至于简单类型不建议使用auto。

1
2
3
4
5
6
7
8
9
10
11
std::vector<double> source;
std::vector<double>::iterator pv = source.begin();

//auto简化
std::vector<double> source;
auto pv = source.begin();

//简单类型不应该使用auto,容易产生歧义
auto x = 0.0;//double 类型
double y = 0;//double 类型
auto z = 0;// int 类型,如果你想把z声明为double类型,就不能赋值为0,而是0.0
  1. array 模板类

vector类的功能比数组强大,但效率较低。

如果需要长度固定的数字,使用数组是最佳的选择,但是不那么方便和安全。

array长度是固定的,使用栈空间,效率与数组相同但是比数组更加方便和安全。

1
2
3
4
5
6
7
#include <array>
using namespace std;
int main()
{
array<int,5> ai={1,2,3,4,5};

}
  1. 右值引用

新增了右值引用,区别于之前的左值引用。这种引用可以指向右值,使用&&声明的

1
2
double && rref = std::sqrt(36.00);
double && jref = 2.0*j+18.5;
  1. decltype 关键字

decltype关键字的出现可以解决C++98中模板出现的问题

在C++98的时候

1
2
3
4
5
6
7
8
template<class T1,class T2>
void ft(T1 x,T2 y)
{
xpy = x+y;
}
// 当x为int,y也为int,xpy为int类型
// 但是当x、y都为类对象时候呢?
//这个时候没有办法确定xpy的类型,也就没有办法声明xpy

C++11新增decltype关键字,可以根据表达式自动推导类型

1
2
3
4
5
6
7
8
int x;
decltype(x) y;//y也为int

decltype(x+y) xpy;//会根据x+y的结果自动推导xpy的类型

//也可以根据函数调用来判断变量类型
int fun1();
decltype(fun1()) p; // 此处并不会调用函数

当然decltype也不是万能的,编译器看到decltype这个关键字,会根据现有条件来判断变量类型,如果条件不足,那么会失败的。

比如:

1
2
3
4
5
6
7
template <class T1,class T2>
?? gt(T1 x,T2 y)
{
return x+y;
}
//编译器无法预知x和y相加得到的类型
//因为如果定义decltype(x+y),但是此时没有x和y参数了,编译器看不到它们也无法使用

为了解决这个问题,没办法,只能新增一个语法,使用后置返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
double h(int x, float y);
//可以写成
auto h(int x,float y) -> double;

//复杂点
template <class T1,class T2>
auto gt(T1 x,T2 y) -> decltype(x+y)
{
return x+y;
}
//这样写法虽然让人觉得很怪异,但是也解决了decltype不能推导出类型的问题
//毕竟C++怪异的写法看多了也就不觉得奇怪了。
  1. 引入关键字 nullptr

在C++98中,有些使用(void*)0来标识空指针,有些使用NULL标识空指针(C语言的宏)。

在C++11中,引入了关键字nullptr,用于表示空指针。建议使用nullptr。

1
char* str = nullptr;
  1. 区间迭代

引入了基于范围的迭代写法,原先的写法:

1
2
3
4
std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
std::cout << *i << std::endl;
}

现在的写法

1
2
3
4
// & 启用了引用
for(auto &i : arr) {
std::cout << i << std::endl;
}
  1. 外部模板

C++11引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使其能够告诉编译器何时才可以进行模板的实例化。(缩短了编译时间)

1
2
template class std::vector<bool>;            // 强行实例化
extern template class std::vector<double>; // 不在该编译文件中实例化模板
  1. 类型别名模板

以前不能给模板另起名字,现在在C++里面可以使用using给模板起别名

1
2
template <typename T>
using NewType = SuckType<int,T,1>;
  1. 默认模板参数

现在C++11支持默认模板参数

1
2
3
4
template<typename T = int,typename U = int>
auto add(T x,U y) ->decltype(x+y){
return x+y;
}
  1. 委托构造

C++11新增委托构造,使构造函数能调用同一个类的其他的构造函数,从而达到简化代码的目的:

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
  1. 继承构造

以前基类想要调用父类的构造函数,那么必须写对应的构造函数才可以调通。如果构造函数太多,导致很麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
A(int i){}
A(double d){}
A(float f){}
...

};
class B:A
{
B(int i):A(i){}
B(double d):A(d){}
B(float f):A(f){}
...
}

现在只需要使用一条语句就可以搞定

1
2
3
4
class B:A
{
using A::A;//这里告诉编译器,你自己搞定,我不写这么多代码了
}
  1. Lambda表达式

Lambda表达式基本语法:

1
[caputrue](param) opt -> ret{body;};

caputrue 是捕获列表

params是参数表(选填)

opt是函数选项,可以填mutable,exception,attribute(选填)。

ret 是返回值类型(选填)

body是函数体

捕获列表控制了lambda表达式能够访问的外部变量以及如何访问变量。

[] 代表不捕获任何变量

[&]捕获外部作用域中所有变量,按引用捕获

[=]捕获外部作用域中所有变量,按值捕获

[=,&foot]按值捕获外部作用域所有变量,按引用捕获foot变量

[bar]按值捕获bar变量,同时不捕获其他变量

[this]捕获当前类中的this指针,如果已经使用& 或 = ,就会默认添加

函数选项

mutable 在按值捕获外部变量时,表明这些捕获的变量可以修改。(必须写参数的小括号)

exception 说明lambda表达式是否抛出异常以及何种异常

attribute 用于声明属性

  1. 单向列表容器 std::forward_list

std::forward_list是一个单向列表容器,使用方法和std::list基本类似,但是不提供size方法。

  1. *无序容器 *

std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。

  1. 元祖 std::tuple

元祖有三个核心的函数:

std::make_tuple 构造元祖

std::get 获取元祖某个位置的值

std::tie 元祖拆包

1
2
3
4
5
6
7
8
9
10
11
12
auto model = std::make_tuple(10, "A", "张三", 20);
auto index = std::get<0>(model);//获取到10
auto two = std::get<1>(model);//获取到A

//元祖拆包
int a, d;
std::string b, c;
std::tie(a, b, c, d) = model;
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
std::cout << d << std::endl;
  1. 正则表达式

C++提供正则表达式库操作std::string对象,使用std::regex进行初始化,通过std::regex_match进行匹配,从而产生std::smatch对象。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>
#include <regex>
int main(){
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 `\` 会被作为字符串内的转义符,为使 `\.` 作为正则表达式传递进去生效,需要对 `\` 进行二次转义,从而有 `\\.`
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// sub_match 的第一个元素匹配整个字符串
// sub_match 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
  1. 左值转右值 std::move

上面简单介绍了左值和右值,C++可以使用std::move将左值转成右值,转换之后,左值不能再使用。一般用在构造函数,代替原先的拷贝构造。

1
2
3
4
5
6
7
8
string(string&& that)
{
data = that.data;
that.data = 0;
}

string x = "5";
string y(std::move(x));
  1. 智能指针

C++提供了三种智能指针类型:

  • shared_ptr:基于引用计数的智能指针,会统计当前有多少对象在使用该指针,计数为0时自动释放
  • weak_ptr:因为循环引用问题,不能单独使用shared_ptr解决,只能引入weak_ptr配合使用,weak_ptr只引用不计数。
  • unique_ptr:遵循独占语义的智能指针,在任何时间点,只能唯一被一个unique_ptr所战友,当其离开作用域自动析构。资源的转移只能通过std::move()而不能通过赋值。
  1. std::function和std::bind

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class Test
    {
    public:
    void Add(std::function<int(int, int)> fun, int a, int b)
    {
    int sum = fun(a, b);
    std::cout << "sum:" << sum << std::endl;
    }
    };
    //Test类中std::function<int(int,int)>表示std::function封装的可执行对象返回值和两个参数均为int类型
    int add(int a,int b)
    {
    std::cout << "add" << std::endl;
    return a + b;
    }

    class TestAdd
    {
    public:
    int Add(int a,int b)
    {
    std::cout << "TestAdd::Add" << std::endl;
    return a + b;
    }
    };

    int main()
    {
    Test test;
    //将函数地址传进去,包含参数
    test.Add(add, 1, 2);

    TestAdd testAdd;
    //将对象的函数地址传进去
    //std::placeholders::_1和std::placeholders::_2 为参数占位符
    //表示std::bine封装的可执行对象可以接受两个参数
    test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2);
    return 0;
    }

评论