再读C++ Primer Plus

C++ Primer Plus

C++,贝尔实验室Bjarne Stroustrup设计的编程语言。
C++ Primer Plus 主要介绍C++98,部分C++11

目录

1 介绍
2 main
3 数据类型
4 复合类型,内存分配
5 循环和关系表达式
6 分支语句和逻辑运算符
7 函数
8 内联函数,引用变量,函数重载,函数模板
9 内存模型和命名空间
10 对象和类
11 使用类,友元函数,运算符重载,构造函数
12 类和动态内存分配,显式拷贝和赋值
13 类继承
14 代码重用,包含关系,类模板,多重公有继承
15 友元,异常
16 string和标准模板库
17 输入、输出和文件
18 C++11
19 附录


第1章

C语言

  • 结构化编程:for循环体、while循环体、do while循环体、if else语句
  • 自顶向下:大型程序拆分为各个小模块
  • 过程化编程:强调算法

面向对象编程

  • OOP:万物皆对象,强调数据
  • 封装:数据+方法。通过方法对数据的访问和处理。
  • 继承:旧类派生新类
  • 多态:
    • 重写:返回值和形参不改变,子类覆盖父类的方法。
    • 重载:多个定义,根据上下文来确定

泛型编程

泛型:generic programming。不必为不同数据类型编写相同的代码。

可移植

兼容C语言,跨平台,不断发展标准。

可执行程序

编译时不必添加头文件,因为编译.cpp时会include进来.h的。

gcc -std=c++11 -c test.cpp test1.cpp #编译得到test.o和test1.o
gcc test.o test1.o -o test #链接成可执行文件test
./test #运行

编译有时候也加入:

gcc -c test.cpp test1.cpp -D DEBUG 让编译中明白DEBUG被define了
gcc test.o test1.o -o test #链接成可执行文件test
./test #运行

在makefile中可参考here

编译

第2章

编译指令

  • #include 将包含的文件一起送给编译器
  • namespace
    • 可访问的范围
      • using namespace std 则有权限的函数可用std内的所有内容
      • using std::cout 则有权限的函数可用cout
      • std::cout 则此处的cout可用
    • 拥有权限的函数
      • 放在函数定义前,所有函数可访问该空间
      • 放在函数定义内,该函数可用
  • #define C语言遗留的宏定义

变量与函数的声明

  • 声明变量

    • 声明变量分类
      • 定义声明:分配内存 int i;
      • 引用声明:引用其他地方已分配的内存 extern int i;
    • 为什么要声明?
      • 指出数据类型,能让编译器知道所需要的内存,并将这块内存单元命名为这个变量名。

      • 动态语言python可能有如下错误。

        data = 12
        date = data + 3 # data手滑成date,实际data未被修改
        

        而假如需要声明,则编译器会报date未声明的错误。

  • 函数
    函数原型:声明时描述的返回值、函数名、输入值
    函数定义:除了函数头,还包含了函数代码

  • 关键词


第3章 处理数据

3.1 整型

C++中规定:

类型 长度
char 一字节
short 至少16位
int 至少short长度
long 至少32位,至少int长度
long long 至少64位,至少long长度

不同系统可能导致数据长度不一致。
climits头文件定义了CHAR_MAXULLONG_MAX,USHRT_MIN等整型的最值,也定义了CHAR_BIT字节的位数。
而运算符sizeof()可以得到变量的数据类型的字节数

# 在MacOS系统测试
CHAR_BIT 8位
sizeof(char) 1字节
sizeof(short) 2字节
sizeof(int) 4字节
sizeof(long) 8字节
sizeof(long long) 8字节

初始化

int data = 1;
int data(1);
int data = {1};
int data{1};
int data = {};//为0

无符号类型
对于sign的类型,非负范围和负数范围一致。
C++中规定长度参见上文有符号整型:

类型 长度
unsigned char
unsigned short
unsigned int
unsigned long
unsigned long long -
  • int是被设置为计算机最自然的长度,处理效率最高。
  • 假如变量大于16位整数,应该用long型,否则在16位系统里,int型会无法正常工作。
  • C++认定整型字面值第一位1~9,则为十进制数。第一位0第二位小于8则为八进制。0x打头则为十六进制。
int data(042);
cout << data << endl;//cout默认输出十进制,所以结果为34
cout << std::oct << data << endl;//控制符std::hex dex oct切换进制,输出42
  • 字面常量默认存为int型。但可以指定:123L则为long型
  • 字符和转义、wchar_t宽字符

3.2 const

应在声明中初始化常量
C++中用const来代替c语言中#define Months 12 的做法。

3.3 浮点数

  • 表示:-1.28或者-12.8e-1
  • 类型:float、double、long double
    |类型|有效位|
    |:-:|:-:|
    |float|至少32位有效位|
    |double|至少64有效位,不少于float|
    |long double|有效位不少于double|

3.4 C++算数运算符

  • + - * / %
  • 类型转换

    //TODO:有点琐碎
    初始化和赋值中、{}方式、表达式中的自动转换、传参时、强制

  • auto:可以在泛型中可以很方便的取出某个值

第4章 复合类型

4.1 数组

int data[5] = {0}
int data[] = {0,2,4}
int num = sizeof(data)/sizeof(int)

4.2 字符串

char str[3] = ('a','b','\0') //a string must end with '\0'
char str[3] = "ab";
//对比
std::cin >> str;//遇到换行符停止。换行符还在
std::cin.getline(str,20);//丢弃换行符

//对比
std::cin.get(str1,5)//遇到换行符停止。换行符还在
std::cin.get()//丢弃刚刚那个换行符
std::cin.get(str2,5)

std::cin.get(str1,5).get(str2,5)
//
//get()读取到空行则设置failbit,cin.clear()
//getline()或get()遇到过长字符串,则设置failbit

4.3 string类

  • string & char array
    但是字符数组不可避免要考虑越界问题
#include <cstring>
strcpy(chars_des,chars_src); // str1 = str2
strcat(chars_des,chars_append); // str1 += str2
strlen(chars);// str1.size()
cin.getline(chars, 20)
getline(cin, str1)

C++11x现有
R"(Jim "m" Tutt \n)"用于raw形式显示string,不必一直转义。并且自定义了定界符"+*()+*"

4.4 结构简介

struct inflatable {
    char[20] name;
    int age;
    int id[2];
};
inflatable tmp = {"test",11,{0,1}};
inflatable tmp1 = tmp;

4.5 共用体

同一块内存的重复利用

union one4all {
    int int_val;
    long long_val;
    double double_val;
}

4.6 枚举

enum spectrum {red, orange, yellow, green};
enum spectrum {red=0, orange=125, yellow=-1, green=255};
//枚举的取值范围

4.7 指针和自由存储空间

  • 易错点
    int *p1,p2;// 等效于 int* p1; int p2;
    
  • 易错点
    不同数据指针本身大小是一样的,但是指针指向的数据大小不一样。
    int* p_intdouble* p_double
  • 易错点
    long *fellow;//fellow is a random address
    *fellow = 223323;//invalid
    

这儿的问题是什么呢?是声明的时候,编译器只会给fellow这个指针分配地址,但是不知道fellow指向哪里,假如指向程序段,那之后再赋值就会把223323写到程序段中,出现bug。
因此,有分配内存的操作。变量的值在stack上,new则是从heap上去分配的。而如果heap满了,new返回的则是null pointer

int *p1 = new int;
int *p2;
p2 = new int; 

相对应的,delete

delete p1;
p1 = null;//避免再次处理p1时,p1指向的内存是有效数据。
  • 易错点
    int *prt;
    prt = (int*)0xB8000000;//需指出整型是int的地址,才能赋值给prt
    

【#annotation#: 我们可以想象,在编译的时候,程序段内的变量表中,对于pointer类型的,应该放了地址,也放了所指向的数据的类型。
至于delete内存,严重的依赖于实现。<<程序员的自我修养中>>最后一章中有一些作者实现的库的代码,用的是额外的空间来记录分配内存的大小。也可能根本不标记长度的。就比如简单的信任程序员的代码,删除的时候把已占用的标志改成未占用。因为某些场合长度都是已知的(比如某些涉及到内存池放置固定大小对象的算法)。也可能把长度专门集中起来标记到一个特殊区域里,那个区域还记录了各起始地址。】


C++可能的编译方式
    char ghost[15] = "galloping";
    char * str = "galloping";
    cout << strlen(ghost)
        << " "
        << strlen(str)
        << " "
        << strlen("galloping") << endl;
  • 动态数组
    声明创建数组,则在编译时分配内存,静态联编,new则为动态联编。
int* psome = new int [10];
delete [] psome;

psome 指向的还是一个int的元素,但是程序段知道 psome 有10个元素,psome=psome[0],psome+1=psome[1]..psome+9=psome[9],但是delete时还是应更从0元素开始!

4.8 指针,数组和指针算术

易错点

double* tmp = array_double;
array_double == &array_double[0] == (array_double + 0) 
// &array_double和array_double指向地址一样,但是意义不一样,前者整个array,后者为1st element的address
double (*tmp)[20] = &array_double;
//double *tmp[20];表示tmp数组有20个double指针
给cout指针,则输出指针,但是对于char*或者char数组,则会打印直到`\0`,用(int*)转化为整数指针才能输出地址。
对于char[],应在初始化时赋值 或 strcpy(),因为声明后再赋值是地址的拷贝
  1. 数组名是指向第一个元素的地址。数组名取址是这个数组的地址。数组下标是指向该元素的指针的解除引用。
  2. 字符串常量指向字符串常量的第一个字符
  3. 函数名指向函数

4.9 类型组合

4.10 数组的替代品vector array

int n = 10;
vector<int> v_int(n); // size=10
vector<int> v_int; // size=0
//vector的效率低于array,因为可以动态调整长度意味着在动态管理内存
//于是在长度固定时,用array效率和int arr[10]一致。
array<int, 5> a_int;//size=5

vector在堆,array和数组在栈,array可以用at(),begin(),end()来检查越界


5 循环和关系表达式

a++;//使用a当前值再加。
重载实现为`operator++(int){}`
//因此对于类定义的后缀格式,需要先复制出副本,再++,再返回原来的副本,效率更低。
++a;//先加,后使用a。前缀自增(++a)。
重载实现为`operator++(){}`
while(guests++<10){//guests 已经增加了,因为条件表达式确保side effect完成
    cout<<guests<<endl;
}

y=(4+ x++) + (6 + x++);//此处分号到了才是完整表达式
//所以只保证了这个分号后,x会被加2,但是具体什么时候加,不确定!
x = *pt++;//++优先级高于解除引用*,即x=*(pt++),x是*pt,但是pt之后指向了下一个元素
string word = "string";
char temp;
int i,j;
for (j=0,i=word.size()-1;j<i;--i,++j){
    temp = word[i];
    word[i] = word[j];
    word[j] = tmp;
}
i = 20, j = 2*i;//j为40是确定的,因为`,`此时也是顺序点。
//且整个表达式的value为`j = 2*i`的value
cats = (17,240);// cats = 240
char word[5] = "mate";
cout<<word == "mate"<<endl;
//数组名是第一个char的地址,字符串常量也是第一个char的地址。因此是不确定的。应该用`strcmp()`比较,返回第一个string字典序(A-Za-z)减去第二个string的。
string word = "mate";
cout<<word == "mate"<<endl;//可以比较,因为string类已重载运算符
clock_t delay = secs * CLOCKS_PER_SEC;
clock_t start = clock();
while(clock()-start < delay) 
    ;
exit();
cin << ch;//忽略空格,换行符,制表符
cin.get(ch);//
cin.fail() //EOF
while(cin.get(ch)){

}
char cities[3][10] = {"Acity","Bcity","Ccity"};//可修改
string cities[3] = {"Acity","Bcity","Ccity"};//可修改
char* cities[3] = {"Acity","Bcity","Ccity"};//不可修改:指针指向字符串常量的首地址
const string cities[3] = {"Acity","Bcity","Ccity"};//不可修改
typedef  char* CP;
#define MAX 999;//replace

6 分支语句和逻辑运算符

if(3 == tmp){
}
if(x <= INT_MAX && x>=INT_MAX){
}

image.png
switch (){
  case label1:statement(s); break;
  case label2:statement(s); break;
  DEFAULT: statement(s);
}
#include <iostream>
#include <fstream>
using namespace std;
int main() {
    ifstream inFile;
    inFile.open("../test.txt");
    if(!inFile.is_open()){
        exit(EXIT_FAILURE);
    }
    string tmp;
    char ch;
    while(inFile.good()){
        inFile.get(ch);
        tmp += ch;
    }
    if(inFile.eof()) cout<< "end of file" << endl;
    else if(inFile.fail()) cout<< "file fails" <<endl;
    else cout<< "unknown reason" << endl;
    inFile.close();

    ofstream outFile;
    outFile.open("test.txt");
    outFile << tmp;
    outFile.close();
    return 0;
}

7 函数

int age = 14;// age可以改变14
cont int *pt = &age;// stack上,pt指向的类型则为const int,不能通过pt改变14
int age1 = 15;
pt = &age1;//可以改变指针本身
int * const pt1 = &age;//指针固定指向age
*pt1 = 15;
//pt1,*pt 是const 的
cont int age = 14;// age不可以改变14
cont int *pt = &age;// stack上,pt指向的类型则为const int,不能通过pt改变14

数据本身是指针,则不要赋予给指向const的指针,如下有错误:

const int **pp2;
int *p1;
const int n =13;
pp2 = &p1;//suppose it could
*pp2 = &n;
*p1 = 10;//change the const data 13

const指针的使用,优点:

  1. 避免无意中修改data
  2. 非const的数据,用const指针,可以满足函数要求的const形参
array2[r][c] == *(*(array2 + r) + c);

函数指针

double function(int variable);
double (*func_pt)(int variable);
func_pt = function;
auto func_autopt = function;

function(5) == (*func_pt)(5) = func_pt(5);
//1 func_pt是指针,*func_pt是函数。
//2 函数名是指向函数的指针,func_pt也是是指向函数的指针。
const double * f1(const double ar[], int n);
const double * f2(const double [], int n);
const double * f3(const double *, int n);
const double * (*pa[3])(const double*, int) = {f1,f2,f3};//pa是一个函数指针数组
const double * (*(*pd)[3])(const double*, int) = &pa;//整个数组的地址
**&pa == *pa == pa[0]//&pa为整个数组的地址
(*pd)[0] == pa[0]//pd是指向一个数组的指针,pa是一个三元素的数组,元素为函数指针
typedef const double *(*p_fun)(const double *,int);
p_fun p1 = f1;
p_fun pa[3] = {f1,f2,f3};
p_fun {*pd}[3] = &pa;

8 内联函数,引用变量&,函数重载OOP同名函数,函数模板

内联函数

inline的代价就是内存占用更多,对于频繁调用庞大函数,这样可能得不偿失,而小而巧的函数,如Max(a,b),用inline效率更高。

#define SQUARE(x) x*x 
实际执行 SQUARE(3+4) => 3+4*3+4 ,明显错误了

引用变量&

int rats = 101;
int &rodents = rats;//相当于int* const pr = &rats;
int * prats = &rats;
// &rodents == prats == &rats

对引用只能初始化声明来设置,不能通过赋值,因为赋值改变的是引用指向的那个别名

double cube(double a){ return a*a*a}
double refcube(const double &a){ return a*a*a}//const使用
cube(x);
refcube(x);//调用时可const可非const

值得一提的是,const引用为函数参数,调用时可const可非const,而且调用者调用方式和值传递一致(不必像指针那样,传入&x)

const int & function(int & target, const int & source);
source传入避免复制,target为返回值,const int & 保证target运算后返回不会被错误地修改。

何时使用引用参数:

  1. 数据小或内置,按值传递
  2. 数组,指针
  3. 大结构,(const or not)(引用 or 指针)
  4. 传递类对象,标准方式是(const or not)引用

默认参数

函数实参传给形参从左往右,因此默认参数右边一定要有才能有左边的。

函数重载OOP同名函数

同名不同参 Function overloading

// modifiable lvalue
void stove(double& r1);//#1  lvalue
// const lvalue
void stove(const double& r2);//#2 lvalue ,const, rvalue
// rvalue
void stove(double&& r3);//#3 rvalue

调用时选择最match的function

double x = 3;
const double y = 4;
stove(x);
stove(y);
stove(x+y);//如果没有#3,将调用#2,

函数模板

具体的函数匹配顺序等有需求可以再细看。

template <typename T>
void Swap(T& a, T& b){
    T temp;
    temp = a;
    a = b;
    b = temp;
}
template <typename T>
T Add(T a, T b){
    return a+b;
}
//显式实例化 explicit instantiation
template double Add<double>(double, double);
//假如传入int,隐式实例化会给template实例int的function,
//而此处显式实例化则能令int=》double
//显式具体化
template <> void Swap<int>(int&, int&){
   //需要自己定义function
}

9 内存模型和命名空间

单独编译

不要将函数定义或变量声明放到头文件中,这样可能导致其他两个文件在include该头文件后,同一个程序include了同一个函数的两个定义,导致出错。

头文件一般放:

  1. 函数原型
  2. #define 或 const:链接属性
  3. 结构声明:不创建变量,在源代码文件中声明结构变量时指导编译器创建
  4. 类声明
  5. 模板声明:指示编译器如何生成函数定义
  6. 内联函数:链接属性

编译时不必添加头文件,因为编译.cpp时会include进来.h的。也因为如此,在.h中用#ifndef非常有必要。同一个文件,只能将同一个头文件include一次!!
a.h

#include <stdio.h>
#include "b.h"

b.h

#ifndef _A_H
#define _A_H 
#include "a.h"
#endif

c.c

#include "a.h"
#include "b.h" //避免重复include "a.h"
int main(){
    printf("Hello!");
}

存储空间

  • 自动存储持续性:声明的变量根据不同的作用域被自动存储和释放。stack?
  • 静态存储持续性:进程的static,或常规静态变量
  • 线程存储持续性:c++11,thread_local,线程级别的常规静态变量
  • 动态存储持续性:new,delete到heap上
    file1.cpp
int global_out_of_file = 3;//不在任何函数内。
一处defining declaration定义声明,处处referencing declaration引用声明
static int in_this_file = 1;
void main(){
}

file2.cpp

使用外部全局变量不必include file1.cpp
extern int global_out_of_file = 3;//在file1.cpp分配了空间
void function(){
}
image.png

cv限定符

const:内存初始化后不能被程序修改。且const全局变量链接性为内部的,如const int states = 5;。可以指定为外部链接,如extern const int states = 5;。其他文件再使用extern即可引用声明该常量。
volatile:即使程序不修改该值,该变量也可能变动(硬件或其他进程)。防止编译器优化而误认为没有变化不重复查找。

mutable

即使结构或类,被const了,成员变量若是mutable,也仍然可以被修改。

函数的链接性

先看该文件的函数原型,如果函数原型是static的,则在本文件找,否则在所有的文件中找函数(不加static默认为外联),如果找到了大于一个定义则出错,如果没找到则到库中寻找。因为必须要include,所以没必要再extern指出引用声明。

new
new []
delete
delete []
new (定位) //用于指点new分配的空间来源。如static char buff[100]
new (buff) int[3];//从buff中分配出3个int,这类定位new不可用delete删除。

命名空间

using namespace std;
namespace PS{
    namespace often{
        int i;
        int k;
    }
int a;
}
int a;//global 
namespace MX{
void main(){
    using namespace PS;
    cin >> a;//PS
    int a;//local
    cin >> a;//local
    cin >> PS::a;//PS
    cin >> ::a//global a
}
int test(){
    using PS::a// 用命名空间的,限定的名称 qualified name  
    int a;//出错 和PS::a同时声明。
}
int test1(){
    using namespace PS::often;
//can not use a 因为a在PS内。需要PS::a
}
}
使用using声明比使用using namespace编译指令,更安全

image.png

image.png

面向对象的三大基本特征

10 对象和类

11 使用类

友元函数 友元类 友元成员函数

class Time{
  friend Time operator* (double m, const Time& t);// 0.75*time;
  Time operator* (double m);// time*0.75;
强制转化。 int(time) 或者 (int)time
  operator int() const;
}
Time operator*(double m, const Time& t){
  //bla
}
Time Time::operator*(double m, const Time& t){
  //bla
}

类的const实例,只能调用const函数,只有const函数能访问const变量

12 类

类中的static被声明,但是应该在类方法的文件中单独出来初始化。首先,不能在类中初始化,因为类不提供内存,而只是声明了分配内存的方法。其次,因为类声明的头文件可能被include,所以不能在.h中初始化。

int StringBad::num_strings = 0;

编译器自动生成(空的):默认构造函数,默认析构函数
危险的编译器自动生成:复制构造函数,赋值运算符,地址运算符

复制构造函数

String(const String&)

String str2(str1);
String str3 = str1;
String str4 = String(str1);
实参传入形参
等等

默认的复制构造函数,是将所有的值都复制了一遍。
因而当成员变量是指向内存的地址时,如果复制构造函数生成的临时变量被析构,则原来的str1的内存会被delete,导致错误。
解决方案:定义显式的复制构造函数,以便完成深拷贝

赋值运算符

String A("test");
String B;
B = A;

和默认的复制构造函数相同的问题,
解决:

  1. 避免目标对象引用了以前分配的数据,即要深拷贝
  2. 避免赋值自身,因为可能需要delete目标对象B,再new给B内存,以便复制A的大小的数据
  3. 返回指向B的引用。

地址运算符

直接返回地址,没问题。

13 类继承 is a

class Date:public BaseClass{
//....
}
Date::Date(int year, int month, int day, int hour, int min): Time(hour, min){
    this.year = year;
    this.month = month;
    this.day = day;
// hour 和min 都传给了基类Time的构造函数
}
  • 无参构造则默认先调用基类的构造函数(类似Java),有参则需要初始化列表
  • 初始化列表也能初始化自身的成员变量,这能加快构造,如不使用,比如成员变量有string,则需要先string默认构造(因为构建对象前,基类和成员变量的默认构造函数会被先一步调用),再通过赋值运算将实参传给string,而初始化列表则调用复制构造函数
  • base class指针可以指向derive class,但是只能使用base class的函数。这样的好处是,形参设为基类,实参可以传入基类和继承类。
  • 但是如果某个函数为virtual函数,则会根据指针指向的实际对象的类型,来决定函数是base class的还是derived class的。
  • 同样,在作为基类时,用virtual析构函数,这样保证调用的是指针指向的实际继承类对象的析构函数,而非基类的。
  • 重载是静态已经决定的,覆盖则是根据实际运行决定的动态联编。
  • 多态的实现是利用每个对象维护一个虚函数表,这样指针调用虚函数时查表,得到最新的虚函数的实现即可。
  • 基类几个虚函数是重载,继承类要么全部覆盖,要么都不覆盖用基类的函数,否则如果只覆盖了几个,另外的几个虚函数将被隐藏而无法使用。
  • protected,对外界private,对继承类是可继承
  • ABC abstract base class 至少拥有一个纯虚函数virtual ……… =0的基类,以便有具体类concrete继承并实现该纯虚函数。
  • 继承类有new时。
    • 赋值运算:显示调用基类的Base::operator=(),为基类的成员赋值,再赋值继承类多出的成员,然后返回继承类*this
    • 复制构造函数:复制继承类的,并将继承类对象初始化列表传给基类(基类引用可以指向继承类,因此可以调用基类的复制构造函数)
    • 析构函数:delete继承类即可。同时会自动调用基类的析构函数
  • 继承关系中的友元函数

14 代码重用

public继承,继承public为public,继承protected为protected,无法继承pivate
protected继承,继承public为protected,继承protected为protected,无法继承pivate
pivate继承,继承public为pivate,继承protected为pivate,无法继承pivate

image.png

包含关系 has a

1个参数调用的构造函数 可以被当成 该参数到类类型的 隐式转换函数
即如果

class Student{
    Student(int n){}
}
可以被
Student A = 5;
隐式调用 Student(5)而变成1个Student类

而如果指明
class Student{
    explicit Student(int n){}
}
则关闭隐式转换函数。让错误出现在编译阶段,而不是在运行阶段。

友元函数是为了让其他类可以使用本类的私有成员变量,因此也可以利用public函数,为其他类提供访问接口,避开友元的限制。

friend ostream & operator<<(ostream& os, const Student &stu){
    os << stu.name;
}
or
ostream & operator<<(ostream& os, const Student &stu){
    os << stu.Name();
}
相当于ostream对象调用的是ostream::operator<<(stu.Name())

私有继承 has a

class Student:private std:string, private std::valarray<double>{
// std::string::size()
}

包含时,使用多个同类型成员变量,并且更明确方便。
但是私有继承时,继承类可以调用基类的protect成员,而包含只有public,并且继承类可以redefine虚函数。

为了让Student类在外界能调用基类的函数,例如private继承时基类函数都成了private,可以
1 利用public的函数包装基类的函数
2 利用声明表明某方法是public的

class Student: private std::string{
public:
    using std::string::size;
};

多重继承

虚基类 class Singer: public virtual Worker{..};避免重复声明Worker,也禁止了中间类把第三代类的值传给基类(因为不知道是从哪一个中间类传的,但是可以利用显示初始化列表,传参给基类,来完成构造)
而对于函数调用,则将每个功能模块化后,由第三代类具体组装使用更合理。

易错点:
对于不是虚基类,用类名限定同名的数据或(非虚函数)方法,避免二义性(非虚函数为静态绑定,会因为指针的类型而调用不用的类的方法,虚函数根据指针的实际对象调用)
对于虚基类,则考虑派生类名称优先于直接或间接祖先类(个人观点:但是不建议这样做,利用类名限定,可读性更强)

类模板

显式具体化:可以声明某种类型,或部分类型(对于Pair等的模板)的模板,并自定义
另 模板类的(非)约束模块友元函数可以具体看书了解。

模板别名。

typedef std::array<double,12> arrd;
typedef std::array<int,12> arri;
or
template<typename T> using arr = std::array<T,12>

2h

15 友元,异常

友元类

利用友元类管理该类,访问protected、private成员。

class TV{
public:
    friend class Remote;
};
class Remote{
public:
    bool shutdown(TV &tv){tv.shutdown();}
};

友元成员函数

为了满足TV类中定义Remote类的函数为友元。

class TV{
    friend void Remote::set_chan(TV&t, int c);
};

需要解决循环依赖:在TV中需要中到Remote是类,set_chan是该类的方法。而Remote中提到TV&t,因此Remote需要知道TV。因此:

class TV;//forward declaration 前向声明
class Remote{...};
class TV{...};

这样才能在TV中知道Remote的方法,而在Remote中知道TV的类型。


image.png

其他友元方式:

class A{
    friend class B;
public:
    void buzz(B & b); //未知B
};
class B{
    friend class A;
public:
    void volup(A & a){ a.vol();} //已经有A及vol的定义
};
inline void A::buzz(B & b){
需要在A声明外定义,并在B之后。
...
}

共同的友元:

class A;
class B{
    friend void func(A& a, B& b);
};
class A{
    friend void func(A& a, B& b);
};
inline void func(A& a, B& b){
...
}

嵌套类

嵌套类B要是在类A的private中,则只有A类知道B。public,protected以此类推。而在A类外创建public中的B,则需要A::B b;

异常

直接异常退出
std::abort();
或者
std::exit();
try{
    if(false) throw "error";
} 
catch(const char* s){
}
catch(const int* i){
}
  • 不建议:异常规范 noexcept
  • 栈解退:异常被throw出来,exception如果没有被该“层”catch,会返回“上一层”,栈中“本层”的内存被自动释放。【throw总是自动创建副本,把异常抛出。因此不必担心异常的内存也被释放】
错误
try{
    double * ptr = new double[3];
    if(false) throw "error";
    delete [] prt; 此处ptr在栈,被释放,而double[3]在堆而没delete
}catch(const char* s){

}
正确
try{
    double * ptr = new double[3];
    if(false) throw "error";
}catch(const char* s){
    delete [] prt;
}
delete [] prt;
  • 基类引用:catch(Base& base),则可以捕获所有的派生而来的异常类,此处引用意义在于多态,而非指向内存,因为异常总是被throw创建副本。也因此应该将这类catch放到最后用来尽可能地捕获异常。
#include<exception>
std::exception 基类
std::stdexcept :
    logic_error 
          domain_error:定义域
          invalid_argument:非法输入
          length_error:长度超出
          out_of_bounds:指示索引越界
    runtime_error
          rang_error
          overflow_error
          underflow_error
bad_alloc异常:在new失败后抛出`bad_alloc`类的异常。#include<new>
空指针: Big* pb = new(std::nothrow) Big[100000];则pb在失败时为空指针

如果异常没有被catch,会调用terminate(),默认terminate()调用abort(),因此可以利用set_terminate()来设置。set_terminate()的参数是输入和返回都void的函数地址。

RTTI

RTTI用来帮助程序员了解基类指针是否能成功指向某个类(虚函数的需求)。RTTI功能需要明确编译器支持,否则可能导致运行阶段的错误!因此,只有在必要的时候,才采取RTTI检验(效率降低)。

  • dynamic_cast<>
Type* base = dynamic_cat<Type *>(ptr)
如果ptr指向的对象(*ptr)类型为Type或者其子类,则转化成功,否则返回空指针。
  • typeid 和 type_info
typeid返回type_info对象的引用。
如果ptr不是空指针,且`ptr`指向String,
typeid(String) == typeid(*ptr)为真

类型转换运算符

可以阅读:BLOG
c++为了严格限制类型转换,利用以下4个类型转换运算符

dynamic_cast  令派生类指针转换成基类指针
Base* base = dynamic_cast(Base*) son_ptr;

const_cast  用于const或volatile特征的变化,用于修改指针
const   High * ptr = &bar;
High * pb = const_cast<High *> ptr;//valid
Low * pl = (Low *)(ptr)//valid 但是不是程序员本意

static_cast
当typename可以被隐式转换为expression,或者expression可被隐式转换为typename,才合法
B * b = static_cast(B*)(a)
A * a = static_cast(A*)(b)
也可以被用于无需类型转换的枚举类到整型

reinterpret_cast

16 string和标准模板库

string

string 是有长度限制的,受string::npos和实际内存空间的影响。
string类实现了7种的构造函数
重载了>,=,<
对于c类型char[]实现和string实现,提供了输入
实现了find()系列的查找函数
详细可见附录F

智能指针

#include<memory>
C++98: auto_ptr
C++11: unique_ptr,shared_ptr
智能指针对象原理是,在指针过期时调用析构函数,delete掉堆上的内存。

  • auto_ptr和shared_ptr用于且只能用于new
  • unique_ptr可以用于new和new[]
  • 因而假如某段数据是栈上的,这段内存的地址不应该传给智能指针的构造函数,因为智能指针无法delete非堆的内存。
  • 同时不能有多个智能指针指向同一个内存,会导致多次delete的错误。赋值符号=,复制构造函数,可以优化:
    • 重载赋值符号=,复制构造函数,为深拷贝
    • 或限制单个智能指针指向同一个内存,对智能指针计数
    • 或入unique_ptr一样在赋值时把ownership传给下一个智能指针,自身不再拥有delete权限
    • 因而auto_ptr不被推荐使用,而unique_ptr会在编译阶段,避免这种智能指针之间的赋值行为(同时利用移动构造函数和右值引用技术,保留了对于临时变量unique_ptr赋值,返回的赋值行为)
  • 选择智能指针
    • 需要多个指向同一对象的指针,用shared_ptr。
      image.png
    • 如果不需要多个指向同一对象的指针。用unique_ptr

STL

模板类可以指定,模板类型和分配器。

  • template <class T, class Allocator = allocator<T>>
    来自定义分配器如何new和delete。
  • 另外STL定义了一些非成员函数给容器类使用,如for_each(), random_shuffle(),sort()
  • for(auto tmp : prices){function_ptr(tmp)} 可替代for_each(prices.begin(),prices.end(),function_ptr);

泛型:容器、迭代器、适配器

什么是迭代器,即容器类中类似于指针作用的,如array.begin(),array.end()返回的都是迭代器,迭代器应该可以++,可以解除引用*

image.png
  • 只要满足要求,如随机访问迭代器满足输入迭代器,那么前者就可以使用为后者设计的算法,因为能够支持该算法的实现。
  • 但是为了性能考虑,应该利用最低功能的迭代器即可。
    所以不同的STL容器类,拥有的迭代器是不同的,如vector可以随机访问,但是List只能双向访问。
  • 【个人观点:这些迭代器特征也是为了满足容器类本身性能特征而设计的】
image.png

image.png

序列vector deque list forward_list queue priority_queue
vector:数组的类表示,动态增减,反转容器 ,结尾位置的插入删除时间是固定的
deque:double-end queue,类似vector,但是在开始位置的插入删除时间是固定的,随机访问,双向queue
list:双向链表,任意位置插入删除时间是固定的。反转容器。无法随机访问。插入不会导致迭代器指向的内容变化。
forward_list:单向list
queue: 不能随机访问,不能遍历,后进前出,查看队首队尾,size,empty等
priority_queue: 最大元素根据priority_queue<int> q(greater<int>)中的greater排到队首。vector实现
stack:不能随机访问。不能遍历。压栈出栈。栈顶。
array:c++11

image.png

关联容器:set map multiset multimap 基于树的

  • set的key是value,因为key的唯一性,所以set的value也需要唯一。关联集合。可反转。有排序。键唯一。可以通过传入比较函数来排序set<string,less<string>> variable_set
const int N = 6;
string s1[N] = {"bb","cc","dd","ee","aa","aa"}:
set<string> A(s1,s1+N); //s1+N是超尾
ostream_iterator<string,char> out(cout, "-");
copy(A.begin(),A.end(),out)
string s2[1] = {"ff"};
set<string> B(s2,s2+1);

set_union(A.begin(),A.end(),B.begin(),B.end(),out);
5个参数都是迭代器,其中最后一个是输出迭代器

set<string> C;
insert_iterator<set<string>> insert2C(C, C.begin());
set_union(A.begin(),A.end(),B.begin(),B.end(),insert2C);
insert2C处,不能放入更直观的C.begin(),因为它是常量迭代器,无法被改变,所以需要重新定义一个输出迭代器
  • multimap
    可反转。有排序。multimap<int, string, less<string>>pair<int, string>

无序关联容器:unordered_set unordered_multiset unordered_map unordered_multimap 基于hash table

函数对象

什么是函数符functor:函数名,指向函数的指针,重载了()运算符的类对象
自适应函数符,函数适配器。

Mark:需要再阅读其他材料。

算法STL


vector valarray Array 是对数组的不同实现。
initializer_list{1,2,3}等能够传给构造函数初始化的原因,是这类大括号隐形成了一个initializer_list对象,该对象传给了容器,同时容器也需要有initializer_list作为参数的构造函数

17 输入、输出和文件

IO

键盘输入根据enter键刷新缓冲区。文件输出利用cout<<flush刷新缓冲区。

cout

  • 对于字符串指针,ostream输出内容,并利用'\0'判断停止。而其他指针则输出地址。因此如果需要字符串的指针,只需要强制转换该指针为其他指针即可。
  • hex等和flush一样是函数,但是重载了插入运算符。cout<<hex。cout是ostream类的一个对象。因此可以hex(cout)。
  • cout.width(12)设置后<<一次即失效,恢复默认。
  • cout.fill('*')替代空格来填充空白处。一直有效
  • cout.precision(2)设置浮点数的精度。一直有效
  • cout.setf(ios_base::showpoint)显示末尾小数点,同样还有ios_base::booalpha显示true false,ios_base::showbase显示基数前缀, ios_base::showpos显示正号, ios_base::uppercase16进制时显示大写。
  • 这些控制符能够自动设置fmtflags。例如 cout.setf(ios_base::hex,ios_base::basefield)根据ios_base::basefield清除了一些相关的控制位,再ios_base::hex设置其中的某一位。
  • cout.unsetf(ios_base::showpoint)即恢复位为0
    image.png

    image.png

cin

iostate包含eofbit badbit failbit

image.png

  • 由于历史原因eofbit或者failbit时, fail()都为真。
  • cout.get(ch)不跳过空白的单字符
  • get()和getline()默认读取整行


    image.png
  • 建议单字符使用cin>>ch或者cin.get(ch)
  • 字符串使用 cin.get(chars,50),cin.getline(chars,50),前者把换行符留在输入流,后者抽取并丢弃换行符。
  • cin.ignore(255,'\n')丢弃255个字符,或者遇到\n
    image.png
  • peek()相当于get()后再putback()read() write()

file IO

  • file.is_open()
file.open("1.txt");
if(!file.is_open()){ exit(-1)}
file.clear();
file.close();
  • new一个流对象,可以依次关联到不同文件
  • int main(int argc, char* argv[]),例如./Test a b c有4个agrv,argv[0]为程序Test
  • bitmask: iostate fmtflag openmode因此可以用位或|进行拼接。
  • ifstream、ofstream都有默认的mode。而fstream没有,需要显式传入。
  • 文本格式适合跨平台读取,二进制存储数值更精确,也适合序列化自定义对象。
ofstream fout("my.dat",ios_base::out|ios_base::app|ios_base::binary)
fout.write((char*)&myclass,sizeof p1)
image.png

image.png
  • tmpnam(&filename)生成临时文件,文件名也是随机
  • fin.seekg(30,ios_base::beg)``fin.seekg(-1,ios_base::cur)``fin.seekg(0,ios_base::end)文件指针偏移
  • fin.tellg() fout.tellp() 当前输入、输出指针

内核格式化

  • 利用sstream为string对象的信息格式化
  • 利用istringstream和ostringstream类使用istream和ostream类的方法处理字符串的数据。

18 C++新标准

C++11

初始化列表,auto,decltype,nullptr,智能指针,别名,返回类型后移,作用域内枚举

移动语义,右值引用

移动构造函数,右值

  • 增加了默认的移动构造函数,移动赋值运算符。
  • default和delete
SomeClass() =default;显式要求编译器提供默认构造/复制赋值构造/复制构造/析构/移动构造/移动赋值函数
SomeClass(const SomeClass &) = delete;显式要求编译器禁要某任意方法。
  • 委托构造函数:构造函数初始化列表使用另一个构造函数
  • 继承构造函数:
class C2:public C1{
public:
    using C1::fn;//可以使用C1中的名为fn的函数
    double fn(double){..};//覆盖了C1中double
}
C2 c2;
int k = c2.fn(3);// uses C1::fn(int)
double z = c2.fn(2.4);//uses C2::fn(double)
  • 重载overload、覆盖(override)、隐藏(overwrite)
    利用final表明该虚函数之后不支持override、override表明之后要做的是override来实现多态而不是隐藏。避免程序员写错。
image.png

lambda

可在函数内定义匿名函数,而不必利用类作为桥梁。

int count3 = 0;
[](int x){ return x%3 == 0}  返回类型会被decltyp自动判断,函数匿名
[](double x)->double{int y=x; return x-y;} 需要指出返回类型
[&count3](int x){ count3 +=  x%3 == 0};利用中括号处理外界变量。

包装器 wrapper 也叫适配器adapter

利用封装,把接受某些形参列表的A函数包装成接受另一种形参列表的B函数。
加入A类重载了()符号,又有函数dub(),他们的返回和输入类型都一直。这时候他们作为函数对像类型是不一致的,我们可以认为的用 function<返回(输入)> f = dubfunction<返回(输入)> f2 = A(3)来让编译器认为他们是一样的。

可变参数模板

template<typename T>
void show_list(T value){
    std::cout<< value;
}
template<typename T, typename... Args>
void show_list(T value, Args... args){
    std::cout<< value << ", ";
    show_list(args...);//将剩下的递归传下去
直到最后,调用另一个show_list,结束。
}

C++11新内容

  • 并行编程:thread_local
#include<atomic>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
  • 新库
#include<random>
#include<chrono>
#include<tuple> 广义的pair
#include<ratio> 有理数算术库
#include<regex> "\\d\\t" 或者 R"\d\t"
  • 低级编程
    更接近底层。
  • 其他
    用户自定义的字面量运算符literal operator
    assert运行时断言
    static_assert 编译时断言
    CRC => UML
    Boost
    OOP

19 附录

计数系统

C++保留字

ASCII字符串

运算符优先级

其他运算符

string

STL


备注

关注C++1x,思考静态语言和编译
现代C++:https://changkun.de/modern-cpp/book/00-preface/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容