放假前去朋友那闲逛,发现他一前端居然看起了一本名为 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等选项。
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。当然,这个行为标准并没有定义,所以还是实现相关的,只不过大部分都是这样实现的。
引用
链接中的练习题里有几个比较有意思的,平时用的比较少。
#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
两个指针指向不同的引用,两个引用引用的同一个变量,最终两个指针的值是一样的。由此可以证明,指针指向的不是引用,而是引用所引用的实际对象。
static_assert
const 指针 typedef
指向常量的指针不能用于改变其所指对象的值,因此要存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14 //pi是个常量,它的值不能改变
double *ptr = π //错误,ptr是一个普通指针
const double *cptr = π//正确
*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 = π //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]#
匿名对象
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操作符,就几乎不可能做到这一点。
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;
}
基于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个字节)