放假前去朋友那闲逛,发现他一前端居然看起了一本名为 C++语言导学 的书,这不是不务正业吗?
为了让他在前端方面能有更深的造诣,我只能把这本书拿来勉为其难的读下。

说来惭愧工作中使用的C++只是最简单的那部分,公司编程规范中明确规定不能使用c++ 11 14 17 模板等新特性。本文记录学习该书过程中的疑问,如有错误欢迎指正。

确定默认c++编译标准

如何确定当前g++使用的哪个版本的C++标准编译呢?
这里介绍一种查看方法,如果当前g++版本是4.7之后,可以使用如下方法查看:

[root workspace]#g++ --version | head -1
g++ (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0
[root workspace]#
[root workspace]#g++ -dM -E -x c++  /dev/null | grep -F __cplusplus
#define __cplusplus 201402L

可以看到我当前的g++编译使用的标准是c++14.
如果想修改g++的标准到指定的标准可以在命令行中添加-std=c++11等选项。
Which C++ standard is the default when compiling with g++?

new和delete

全局替换new和delete

#include <cstdio>
#include <cstdlib>
// 函数最小集的替换:
void* operator new(std::size_t sz) {
    std::printf("global op new called, size = %zu\n",sz);
    return std::malloc(sz);
}
void operator delete(void* ptr) noexcept
{
    std::puts("global op delete called");
    std::free(ptr);
}
int main() {
     int* p1 = new int;
     delete p1;
 
     int* p2 = new int[10]; // C++11 中保证调用替换者
     delete[] p2;
}

运行结果:

[root c++]#./a.out
global op new called, size = 4
global op delete called
global op new called, size = 40
global op delete called

类特定重载

#include <iostream>
// 具大小的类特定解分配函数
struct X {
    static void operator delete(void* ptr, std::size_t sz)
    {
        std::cout << "custom delete for size " << sz << '\n';
        ::operator delete(ptr);
    }
    static void operator delete[](void* ptr, std::size_t sz)
    {
        std::cout << "custom delete for size " << sz << '\n';
        ::operator delete(ptr);
    }
};
int main() {
     X* p1 = new X;
     delete p1;
     X* p2 = new X[10];
     delete[] p2;
}

可能的输出结果:

custom delete for size 1
custom delete for size 18

这里输出原因的解释:
应该是编译器在new分配数组内存的时候,前面多分配了一个整数大小的空间用来存储数组长度,不然delete []操作不能确定数组大小。这个大小和机器有关,你应该是在64位机器上验证的,所以这个长度用64位存储,即8字节。struct没有成员,空间占用为0,但是编译器会给它分配1字节的空间(如果不分配空间的话,var2[0]和var2[1]的地址就是一样的了,这是标准不允许的)。结果就是8+10*1。当然,这个行为标准并没有定义,所以还是实现相关的,只不过大部分都是这样实现的。

operator_delete

引用

references in c++
链接中的练习题里有几个比较有意思的,平时用的比较少。

#include<iostream> 
using namespace std; 

void swap(char * &str1, char * &str2) 
{ 
char *temp = str1; 
str1 = str2; 
str2 = temp; 
} 

int main() 
{ 
char *str1 = "GEEKS"; 
char *str2 = "FOR GEEKS"; 
swap(str1, str2); 
cout<<"str1 is "<<str1<<endl; 
cout<<"str2 is "<<str2<<endl; 
return 0; 
} 

函数swap(char * &str1, char * &str2) 中参数从右向左读,可以看到str1,str2是引用,是什么类型的引用呢? char *的引用,和int的引用等是一样的道理。这样函数swap就能达到交换指针值的目的。

引用不是对象,没有地址,因此不能定义指针指向引用。
C++ primer 第五版,2.3.3章节里:
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
如下程序编译失败:

#include<iostream> 
using namespace std; 

int main() 
{ 
int x = 10; 
int *ptr = &x; 
int &*ptr1 = ptr; 
} 

编译错误提示:

[root leetcode]#g++ c++.cc
c++.cc: In function ‘int main()’:
c++.cc:8:7: error: cannot declare pointer to ‘int&’
 int &*ptr1 = ptr;
[root leetcode]#c++decl
Type `help' or `?' for help
c++decl> explain int &*p
declare p as pointer to reference to int

p是指针,指向引用,虽然不能直接声明指针指向引用,但是我们可以间接的,示例程序如下。

#include <stdio.h>
#include <stdlib.h>

int main() 
{
  int a = 0;
  // two references referring to same object
  int& ref1_a = a;
  int& ref2_a = a;

  // creating a different pointer for each reference
  int* ptr_to_ref1 = &ref1_a;
  int* ptr_to_ref2 = &ref2_a;

  printf("org: %p 1: %p 2: %p\n", &a, ptr_to_ref1, ptr_to_ref2);
  
  return 0;
}

运行结果:

[root leetcode]#./a.out
org: 0x7ffc3a0a7b14 1: 0x7ffc3a0a7b14 2: 0x7ffc3a0a7b14

两个指针指向不同的引用,两个引用引用的同一个变量,最终两个指针的值是一样的。由此可以证明,指针指向的不是引用,而是引用所引用的实际对象。
pointer to reference

static_assert

Understanding static_assert in C++ 11

const 指针 typedef

指向常量的指针不能用于改变其所指对象的值,因此要存放常量对象的地址,只能使用指向常量的指针。

const double pi = 3.14  //pi是个常量,它的值不能改变
double *ptr = &pi;   //错误,ptr是一个普通指针
const double *cptr = &pi;//正确
*cptr = 50;//错误,不能给*cptr赋值

例外的情况:
允许一个指向常量的指针指向一个非常量对象。

double dval = 3.14;
cptr = &dval;//可以让cptr指向dval,但是不能通过cptr改变dval的值

常量指针必须初始化,而且一旦初始化完成,则它的值就不能再改变,即不变的是指针本身的值而非指向的那个值。

int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = &pi; //pip是一个指向常量对象的常量指针

介绍了这么多,其实主要为了引出下面我经常忽略的一个点:

typedef char * pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针

上面两条声明语句的基本数据类型都是const pstring,const是对给定类型的修饰。pstring实际上指向char的指针,因此const pstring就是指向char的常量指针,而非指向常量字符的指针。

遇到类型别名时,人们往往会错误地尝试把类型别名展开,以理解该语句的含义,这是错误的!
例如:

typedef char * pstring;
const pstring cstr = 0;
展开成:
const char * cstr = 0;//是对const pstring cstr 的错误理解

声明语句中用到的pstring时,其基本数据类型是指针。可是展开之后数据类型就变成了char,星号成了声明符的一部分。这样展开的结果是,const char成了基本数据类型。展开前声明了一个指向char的常量指针,展开后则声明了一个指向const char的指针。

验证程序如下:

#include <bits/stdc++.h>

using namespace std;

int main()
{
    char arr[10] = "Hello";
    typedef char *pstring;
    const pstring cstr = 0;
    const pstring *ps;

    cout << std::is_same<decltype(cstr), char *const>::value << endl;
    cout << std::is_same<decltype(ps),char *const *>::value << endl;

    //cstr = arr;//错误,cstr是一个指向char的常量指针,不变的是指针本身的值而非指向的那个值
    return 0;
}

如果使用typeid验证类型的话需要注意typeid会忽略顶层的cv-qualifier(const或volatile).验证程序:

#include <bits/stdc++.h>

using namespace std;

int main()
{
    typedef char *pstring;
    const pstring cstr = 0;
    const pstring *ps;

    cout << typeid(cstr).name() <<endl;
    cout << typeid(ps).name() <<endl;

    return 0;
}

执行结果:

[root workspace]#./a.out
Pc
PKPc
[root workspace]#
[root workspace]#./a.out | c++filt -t
char*
char* const*
[root workspace]#
[root workspace]#echo PKPc | c++filt -t
char* const*
[root workspace]#echo Pc | c++filt -t
char*
[root workspace]#

匿名对象

anonymous-objects

p5 单引号作为数字分隔符

为了令长字面量对人类更易读,我们可以使用单引号作为数字分隔符。

#include <iostream>

using namespace std;

int main()
{
    double pi = 3.14'15'92'65'35;
    cout << pi <<endl;
    return 0;
}

我们编译运行上面的例子:

[root workspace]#g++ -std=c11 c++.cc
cc1plus: warning: command line option ‘-std=c11’ is valid for C/ObjC but not for C++
[root workspace]#g++ -std=c++11 c++.cc
c++.cc:7:21: warning: multi-character character constant [-Wmultichar]
     double pi = 3.14'15'92'65'35;
                     ^~~~
c++.cc:7:27: warning: multi-character character constant [-Wmultichar]
     double pi = 3.14'15'92'65'35;
                           ^~~~
c++.cc: In function ‘int main()’:
c++.cc:7:21: error: expected ‘,’ or ‘;’ before '\x3135'
     double pi = 3.14'15'92'65'35;
                     ^~~~
[root workspace]#
[root workspace]#
[root workspace]#g++ -std=c++14 c++.cc
[root workspace]#
[root workspace]#g++ c++.cc

由此可以验证我们当前所使用的g++默认使用的编译标准是c++14.
使用单引号作为数字分隔符是C++14之后才开始支持的。
16.2.2章节可以看到使用单引号作为数字分隔符是C++14引入的标准。

如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则他们实际是上一个整体,当书写的字符串字面值比较长,写在一行里不太合适时,就可以采取分开书写的方式:

std::cout <<"a really long string literal "
                <<"that spans two line"<<std::endl;

编译运行下面两个代码,运行结果一致:

#include <iostream>

int main()
{
    std::cout <<"a really long string literal "
                <<"that spans two line"<<std::endl;
    return 0;
}
#include <iostream>

int main()
{
    std::cout <<"a really long string literal that spans two line"<<std::endl;
    return 0;
}

p7 初始化列表避免类型转换

#include <iostream>
using namespace std;
int main()
{
    int i1=7.8;
    int i2 {7.8};
    return 0;
}

编译报错:

[root workspace]#g++ -std=c++11 cc.cc
cc.cc: In function ‘int main()’:
cc.cc:8:16: error: narrowing conversion of ‘7.7999999999999998e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
     int i2 {7.8};

使用{}能够避免在类型转换中丢失信息。丢失信息的类型转换(double 转换为 int,int转换为char等),在C++中是允许的,而且是隐式应用的。之所以支持隐式转换是为了与C语言保持兼容。

p7 auto关键字

编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候需要清楚地知道表达式的类型。然而做到这一点并非那么容易,有时候甚至做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i = 0,*p = &i;    //正确:i是整数、p是整型指针
auto sz = 0,pi = 3.14;    //错误:sz和pi的类型不一致

我们使用几个例子说明auto的用法:

// C++ program to demonstrate working of auto 
// and type inference 
#include <bits/stdc++.h> 
using namespace std; 

int main() 
{ 
    auto x = 4; 
    auto y = 3.37; 
    auto ptr = &x; 
    cout << typeid(x).name() << endl 
        << typeid(y).name() << endl 
        << typeid(ptr).name() << endl; 

    return 0; 
} 

运行情况:

[root workspace]#./a.out
i
d
Pi
[root workspace]#
[root workspace]#
[root workspace]#
[root workspace]#./a.out  | c++filt -t
int
double
int*

auto的作用最常用在处理迭代器类型时:

// C++ program to demonstrate that we can use auto to 
// save time when creating iterators 
#include <bits/stdc++.h> 
using namespace std; 

int main() 
{ 
    // Create a set of strings 
    set<string> st; 
    st.insert({ "geeks", "for", "geeks", "org" }); 

    // 'it' evaluates to iterator to set of string 
    // type automatically 
    for (auto it = st.begin(); it != st.end(); it++) 
        cout << *it << " "; 

    return 0; 
} 

运行结果:

[root workspace]#./a.out
for geeks org [root workspace]#

如果不使用auto需要使用对应类型的迭代器,程序如下:

// C++ program to demonstrate that we can use auto to
// save time when creating iterators
#include <bits/stdc++.h>
using namespace std;

int main()
{
    // Create a set of strings
    set<string> st;
    st.insert({ "geeks", "for", "geeks", "org" });

    for (set<string>::iterator it = st.begin(); it != st.end(); it++)
        cout << *it << " ";

    return 0;
}

一个例子说明auto与decltype的区别:

#include <iostream>
int global = 0;
int& foo()
{
   return global;
}

int main()
{
    decltype(foo()) a = foo(); //a is an `int&`
    auto b = foo(); //b is an `int`
    auto &c = foo(); //c is an `int&`

    b = 2;

    std::cout << "a: " << a << '\n'; //prints "a: 0"
    std::cout << "b: " << b << '\n'; //prints "b: 2"
    std::cout << "c: " << global << '\n'; //prints "c: 0"
    std::cout << "global: " << global << '\n'; //prints "global: 0"
    std::cout << "---\n";

    //a是global的引用
    a = 10;

    std::cout << "a: " << a << '\n'; //prints "a: 10"
    std::cout << "b: " << b << '\n'; //prints "b: 2"
    std::cout << "c: " << global << '\n'; //prints "c: 10"
    std::cout << "global: " << global << '\n'; //prints "global: 10"

    //c是global的引用
    c = 20;
    std::cout << "a: " << a << '\n'; //prints "a: 20"
    std::cout << "b: " << b << '\n'; //prints "b: 2"
    std::cout << "c: " << global << '\n'; //prints "c: 20"
    std::cout << "global: " << global << '\n'; //prints "global: 20"
    return 0;

}

运行结果:

a: 0
b: 2
c: 0
global: 0
---
a: 10
b: 2
c: 10
global: 10
a: 20
b: 2
c: 20
global: 20

auto与decltype共用的例子:

int& foo(int& i);
float foo(float& f);

template <class T> 
auto transparent_forwarder(T& t) −> decltype(foo(t)) {
  return foo(t);
}

在编程时,程序员有时需要编写一个泛型转发函数,使之不论以何种类型实例化,都能返回同于包装函数的类型,而若无decltype操作符,就几乎不可能做到这一点。
auto
decltype vs auto
decltype

lambda表达式

lambda表达式形式:

[capture list] (params list) mutable exception-> return type { function body }

各项具体含义如下:

capture list:捕获列表
params list:参数列表
mutable指示符:用来说用是否可以修改捕获的变量
exception:异常设定
return type:返回类型
function body:函数体

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。

auto f =[] {return 42;}

lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符;

cout <<  f() <<endl;//打印42
向lambda传递参数

作为一个带参数的lambda的例子,我们可以编写一个与isShorter函数完成相同功能的lambda:

[](const string &a,const string &b){return a.size() < b.size();}
使用捕获列表
[sz](const string &a){return a.size() >= sz;};

例子:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool cmp(int a, int b)
{
    return  a < b;
}

int main()
{
    vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
    vector<int> lbvec(myvec);

    sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
    cout << "predicate function:" << endl;
    for (auto val : myvec)
        cout << val << ' ';
    cout << endl;

    sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });   // Lambda表达式
    cout << "lambda expression:" << endl;
    for (auto val : lbvec)
        cout << val << ' ';
}

在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。

值捕获
#include <iostream>
using namespace std;

int main()
{
    int a = 123;
    auto f = [a] { cout << a << endl; };
    f(); // 输出:123

    a = 234;
    f(); // 输出:123
    return 0;
}

程序输出结果:

[root c++]#./a.out
123
123

类似参数传递,变量的捕获方式也可以是值或引用。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。

引用捕获
#include <iostream>
using namespace std;

int main()
{
    int a = 123;
    auto f = [&a] { cout << a << endl; };
    f(); // 输出:123

    a = 234;
    f(); // 输出:234
    return 0;
}

运行结果:

[root c++]#./a.out
123
234
隐式捕获

除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。支持值捕获和引用捕获混用,支持显示捕获和隐式捕获混用。示例代码:

void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os=cout,char c = ' ')
{
    //os隐式捕获(引用捕获方式),c是显示捕获(值捕获方式)
    for_each(words.begin(),words.end(),
                [&,c](const string &s){os << s << c ;});
   //c隐式捕获(值捕获方式),os是显示捕获(引用捕获方式)
   for_each(words.begin(),words.end(),
                [=,&os](const string &s){os << s << c ;});
}
修改捕获的值

默认情况下对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。

void fcn3()
{
    size_t v1 = 42; //局部变量
    // f可以改变它所捕获的变量的值
    auto f = [v1]() mutable {return ++v1;};
    v1 = 0;
    auto j = f();//j为43
}
void fcn4()
{
    size_t v1 = 42;
    auto f2 = [&v1]{return ++v1;};
   v1 = 0;
    auto j = f2();//j为1
}
指定lambda返回类型

C++ primer 第5版关于lambda返回类型的介绍:如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。
示例代码:

#include <bits/stdc++.h>
using namespace std;


int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    int n = sizeof(arr)/sizeof(arr[0]);

    transform(arr, arr+n, arr, [](int i){if(i<0) return -i;else return i + 1;});

    for (int i=0; i<n; i++)
        cout << arr[i] << " ";

    return 0;
}

godbolt
基于gcc 4.6.4测试如果不指定返回值则编译报错。
修改后的代码如下:

    transform(arr, arr+n, arr, [](int i)->int{if(i<0) return -i;else return i + 1;});

从 c++14 开始就不是这样了。没有 trainling return type 的 lambda 返回 auto ,由函数体推断。
可以在 gcc 4.6 里看到 c++11 的行为。最后一个版本是 4.6.4,2013.4.12 发布。
从 gcc 4.7 开始(4.7.0 2012.3.22 发布),c++11 的这一个行为已经不再支持,即使指定 -std=c++11。

p 193 void *指针

void *是一种特殊的指针类型可用于存放任意对象的地址。一个void *指针存放着一个地址,这一点和其它指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:

double obj = 3.14,*pd = &obj;
void *pv = &obj;
pv = pd;//pv能存放任意类型的指针

概括说来,以void *的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。

在C中,void *可用来为任何指针类型的变量赋值或初始化,但在C++中则可能不行的,参考以下例子,在C++中编译失败,C中则编译成功。
C++代码:

#include <iostream>
using namespace std;

int main()
{
    char ch = 'a';
    void *pv = &ch;
    int *pi = pv;
    *pi = 666;
    return 0;
}

C代码:

#include <stdio.h>

int main()
{
    char ch = 'a';
    void *pv = &ch;
    int *pi =pv;
    *pi = 666;
    return 0;
}
    char ch = 'a';
    void *pv = &ch;
    int *pi =pv;//c++中这里会编译报错,避免了下面的问题,但是可以使用(int *)强转后赋值
    *pi = 666;//覆盖了ch和后面3个字节中的数据(假设int占4个字节)