C++笔记(自用)

C++基础

语法基础

字符

算术类型

整型,浮点型,字符型

有符号数,无符号数:在类型转换时需要操作人员确认是否会出现问题,切勿混用unsigned和有符号

标识符:字母或下划线开头,只可由字母、数字和下划线构成

字符型变量: '单字符' "字符串"

转义字符:

\n 换行

\ \反斜杠标

\b退格符

\r回车符

\t 水平制表符

三目运算符:a>b ? a : b

语句

switch语句:switch() {case x: break;}

条件语句: if(){} else if(){} else{}

循环语句:

for循环

while循环

嵌套循环

跳转语句:

break 语句

跳出循环语句

跳出switch语句

continue 语句:执行到本行不再执行后面代码直接进入下次循环

go to 语句:直接跳到标记的位置

指针

指针指向一个元素,指针本身存着被指向元素的地址

int *p = NULL;//空指针(初始化)
int a = 10;
p = &a;//指针赋值
std::cout<<*p<<p<<std::endl;//*解引用,p的值是一个地址
std::cout<<sizeof(p)<<std::endl;//32位系统指针大小为4B,64位系统指针大小为8B

使用指针时记得初始化

空指针:NULL 初始化用,不能访问。

野指针:指针变量指向非法的内存空间。野指针和空指针指向的都是我们未申请的空间,不要访问!

const修饰指针:

修饰指针:常量指针,指针指向不可以更改

修饰常量:指针常量,指针指向的值不可以更改

都修饰:两个都不能更改

指针的自增:指针的自增是根据指向的数据类型所改变的,如指向int型,自增为4

void* 指针:一个void* 指针存放一个地址,void* 指针可以存放任意数据类型的指针。但是由于我们在使用时不知道void*存放的地址指向的数据类型到底是什么,所以不能确定可以在这个对象上做哪些操作,不能直接对这个对象操作

引用

引用就为一个已知的对象起一个别名

int a = 10;
int &r = a;//引用赋值

引用本身不是一个对象,不能定义引用的引用!

const限定符

const

const修饰的变量,这个变量的值不可以改变

初始化:尽管我们定义了整型常量,但是常量特征仅仅在执行改变他的操作才会发挥作用,而初始化时不需要考虑它是不是应该常量

int i = 30;
const int ci = i;//正确-初始化拷贝不考虑ci是不是常量

const对象只在文件内有效(如果想在多个文件共享const对象1,需要在变量定义前加extern关键字)

const的引用

引用不是对象,只存在对常量的引用这个常量问题

对常量的引用不能被用作修改它所绑定的对象

const int ci = 1024;
const int &cr = ci;//正确
//int &a = ci;错误

对const的引用可能引用一个非const的对象:常量引用仅仅对可参与的操作做出限定,对于引用对象本身是不是个常量未作限定,因此对象可能是给非常量,所以对象是可以通过其他途径改变它的值

int i = 30;
const int &ci = i;//对象可以是非常量
//ci = 40;不能通过这个引用修改对象值
i = 40;//对象是可以通过其他途径改变

指针和const

因为指针本身是一个对象,所以存在两种常量问题

指针常量

与引用一样,也可以使指针指向非常量和常量,指向常量的指针不能用于改变其所指的对象。指向常量的指针也没有规定其所指的对象必须是一个常量,所以指针常量仅仅不能通过指针修改对象的值,对象可以通过其他途径改变值

int i = 30;
const int *ci = &i;//对象可以是非常量
//*ci = 40;不能通过这个引用修改对象值
i = 40;//对象是可以通过其他途径改变
常量指针
int i= 30;
int *const ic = &i;//指针永远指向i,指针的指向不可以改变
*ic = 40;//正确,i本身不是常量,可以通过常量指针修改

顶层const

顶层const表示指针本身是个常量

底层const表示指针指向的对象是个常量

auto关键字

auto关键字可以让编译器替我们取分析表达式所属的类型。

auto关键字会使c++的强类型弱化,所以基本上很少使用,在迭代器或者其他较长的类型表达中可以使用auto简化代码

int val1 = 10,val2 = 20;
auto val = val1 +val2;//编译器推断val的类型
std::vector<int> v;
std::vector<int>::std::iterator it = v.begin();
std::auto it =v.begin();//代码简洁很多

auto关键字会忽略顶层const特性

const int ci = i, &cr = ci;
auto a = ci//这里a是一个整型,而不是整数常量,顶层const特性被忽略
auto b = cr;//同样的,这里b是一个整型,而不是整数常量
​
const auto c = ci;//如果希望auto推断出来是一个顶层const,需要前面加上const   
auto& d = ci;//d是一个整型常量引用 底层const保留下来

函数

函数封装与调用

函数引入可以减少代码的重复量,使编写简单和代码更加简洁

封装:1.返回值类型 2.函数名 3.参数列表 4.函数体语句 5.return表达式

调用:函数名(参数列表);(有返回值需要定义数据结构去接受

值传递与地址传递

值传递

调用函数时的参数为实参,函数定义时参数为形参。值传递时,形参只是拷贝了实参的值,在内存中是两块地,所以函数中对形参值的改变影响不到实参

void swap(int num1 ,int num2)//值传递
{
    int temp = 0;
    temp = num1;
    num1 = num2;
    num2 = temp;
}
int main()
{
    int a=10,b=20;
    swap(a,b);
    std::cout<<a<<b<<std::endl;//输出结果还是10 20 
}
地址传递

使用引用或者指针的传递,地址传递传递的指针,形参和实参两个指针指向同一个地方,所以形参对目标的修改是可以到实参的

void swap(int* p1 ,int* p2)//地址传递
{
    int temp = 0;
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main()
{
    int a=10,b=20;
    swap(&a,&b);//&取地址符
    std::cout<<a<<b<<std::endl;//输出结果20 10 
}

结构体

结构体属于用户自定义的数据结构,允许用户存储不同的数据类型

struct 结构体名{结构体成员列表} ,struct定义的默认是public,所以public声明可以忽略

struct Student
{
    std::string s_name;
    int s_age;
    int s_id;
}
结构体数组:

struct Stuarr sarr[3] = {{},{},{}};
结构体指针:

Student s1("张三",18,10086);
Student *s = &s1; //指针赋值
std::cout<<s->s_name<<std::endl;

封装

访问权限

访问权限(class默认为私有)

public 公共 :类内可以访问,类外可以访问,儿子可以访问

protected 保护:类内可以访问,类外不可以访问,儿子可以访问

private 私有:类内可以访问,类外不可以访问,儿子不可以访问

将所有成员设为私有,可以通过类内成员函数来访问,就可以控制读写权限

对象初始化

构造函数 :没有返回值,有参数,可以重载。

构造分裂:

普通构造,拷贝构造

有参构造,无参构造

析构函数:不用写void,没有返回值和参数,不能发生重载。

class Person
{
public:
	std::string p_name;
    int p_age;
    Person(std::string name,int age)//构造函数
    {
        this->p_name = name;
        this->p_age = age;
    }
    `Person(){}//析构函数
};

//注意!!!
Person p1;
Person p2 = p1;//拷贝构造,有新对象被定义就一定会调用构造函数
Person p3;
p3 = p1;//没有新对象定义,调用赋值操作
拷贝构造调用

浅拷贝:简单赋值的拷贝操作

深拷贝:在堆区重新申请,进行拷贝操作

class Person{
    person(int age,int height){
        p_age = age;
        p_height = new int(height);
    }
    Person(const Person& p){
        this->p_age = p.p_age;//浅拷贝
        this->p_height =new int(*p.p_height);//深拷贝
    }
    int p_age;
    int *p_height;//指针
}
静态成员

在成员变量和成员函数前面加上关键词static,称为静态成员

静态成员变量:所有对象共享同一份数据,在编译阶段分配内存,类内声明,类外初始化

静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量,不可以访问非静态变量

静态成员可以通过对象访问,也可以通过类名访问(::)

class Person{
public:
	static int p_a;//类内声明
}
int main(){
    int Person::p_a = 100;//类外初始化
}
this指针

this指针指向被调用成员函数所属的对象,一般隐含,特殊情况用于区分形参与成员属性同名的情况

this还用于在类的非静态成员函数中返回对象本身,可用return *this;

class Person{
public:
	int p_a;//类内声明
    //Person& 才会返回p1本身,没有&会重新创建一个Person
    Person& personadd(Person &p){
        this->p_a+=p.p_a;
        return *this;//返回本身
    }
}
int main(){
    Person p1(10);
    Person p2(20);
    Person p3(30);
    Person p4(40);
    p1.personadd(p2).personadd(p3).personadd(p4)//链式思维
}
友元friend

三种使用友元情况:

全局函数做友元:friend void function();在类中声明全局函数是friend,这样全局函数可以访问类中的隐私部分

类做友元:friend class GoodGay;

成员函数做友元:friend void GoodGay::visit();

class Gay1{
friend class Gay2::showGoodFriend();//声明友元
public:
    int g_a;
private:
    int g_b;//可以被友元成员函数访问
}
class Gay2{
public:
    showGoodFriend(Gay1& g1){
        std::cout<<g1.g_b<<g1.g_a<<std::endl;
    }
}
运算符重载

返回值类型 operator 要重载的运算符(参数列表){}

加号运算符重载+

成员函数重载和全局函数重载均行,重载后对自定义的类也可以进行+-运算

左移运算符重载<<

注意链式思维,只用全局函数进行重载

ostream&  operator<<(ostream &cout,Person &p){
 std::cout<<p.p_a<<p.p_b<<std::endl;
 return cout;
} 
int main(){
	Person p1;
 std::cout<<p1;//可以对自定义数据结构进行cout输出
}

递增运算符重载++

注意++前置和后置的递增重载不一样

class Myprint{
public:
 //后置重载
	Myprint operator++(int){//int为占位参数,可以用于区分前置和后置
     myprint temp =*this;
     m.Num++;
     return temp;
 }
 //前置重载
 Myprint& operator++(){
     m.Num++;
     return *this;
 }
private:
	int nums;
}

赋值运算符重载=

注意成员属性为指针时,在重载时记得加判断this->p是否为空,不为空记得初始化

p1=p2后可能会造成堆区内存重复释放的问题,为了避免堆区重复释放采用深拷贝

关系运算符重载==,<,>...

使两个自定义的对象进行对比

函数调用运算符重载()

对于()的重载后非常像函数的调用,所有称为仿函数

继承

继承方式

公共继承:父类中除了私有的其他全部照搬到子类

保护继承:父类中除了私有的其他全部搬到子类并全改为protected权限

私有继承:父类中除了私有的其他全部搬到子类并全改为private权限

class vater{};//父类
class Son:public Vater{//公共继承
public:
    void content{};//子类独特特性
};
继承中的对象模型

父类中的所有非静态成员属性都会被子类继承下去,但是私有成员被编译器隐藏了,因此访问不到

可以用VS开发人员命令符来看继承了哪些内容

继承子类父类同名情况

子类访问父类的成员需要加上作用域 Vater::

静态成员用类名访问也加上作用域 Son::Vater::test();

菱形继承

a->b a->c c,b->d d继承了两份a的数据会报错----加上作用域可以解决报错问题

对于资源浪费的问题---可以使用虚继承来解决资源浪费的问题

class A{
public:
    int m_a;
}
class B:virtual public A{};//虚继承virtual
class C:virtual public A{};
//BC中存的虚基类指针vbptr,vbptr指向虚基类表vbtable,表中记录偏移量,加上偏移量后可以找到m_a
class D:public B,public C{};

多态

概念

静态多态:函数重载 and 运算符重载,静态多态地址早绑定,编译阶段确定函数地址

动态多态:派生类和虚函数实现运行时多态,动态多态地址晚绑定,运行阶段确定函数地址

实现动态多态

先决条件:父类指针指向子类对象&&子类重写父类中的虚函数

using namespace std;
class Animal{
public:
    virtual void speak(){//虚函数
        cout<<"动物在说话"<<endl;
    }
};
class Cat:public Animal{
public:
    void speak(){//子类继承后重写同名成员函数
        cout<<"喵喵喵"<<endl;
    }
}
class Dog:public Animal{
public:
    void speak(){//子类继承后重写同名成员函数
        cout<<"旺旺旺"<<endl;
    }
}
void testspeak(Animal &ani)//参数定义一个父类指针
{
    ani.speak();
}
int main()
{
    Cat c1;
    Dog d1;
    testspeak(c1);//()加的子类对象,发生了父类指针指向子类对象
    testspeak(d1);//()加的子类对象,发生了父类指针指向子类对象
}
纯虚函数和抽象类

在多态中,通常父类中的虚函数的实现是毫无意义的,主要是调用子类重写的内容,因此可以将虚函数改为纯虚函数

class Animal{
public:
	virtual void speak()=0;//纯虚函数
}

有纯虚函数的类是无法实例化对象的,所以这个类也称为抽象类,子类必须重写抽象类中的纯虚函数,否则也属于抽象类

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,因为父类指针只能调用子类继承的那一部分,无法调用子类的析构函数

将父类中的析构函数改为虚析构或纯虚析构可以调用子类的析构函数

virtual `Animal()=0;//纯虚析构

内存分区模型

代码区:存放函数体二进制代码,由操作系统进行管理

存放机器指令,共享,只读

全局区:存放全局变量和静态变量以及常量

存放全局变量和静态变量,包含常量区,字符串常量和其他常量。该区域的数据在程序结束后由系统释放

栈区:由编译器自动分配释放,存放函数的参数值,局部变量等等

局部变量,const修饰的局部变量,函数参数

在函数定义不要返回局部变量地址,返回的地址指向的局部变量函数执行完就被释放了,所以这个地址无效

堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

程序员通过new关键字在堆区开辟内存

文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束这些数据都会释放,通过文件操作可以将数据持久化

对文件操作加头文件 #include<fstream>

操作文件三大类:

ofstream写操作

ifstream读操作

fstream读写操作

文本文件

文件打开方式
ios::in //读文件打开文件
ios::out//写文件打开文件
ios::ate//初始位置:文件尾
ios::app//追加方式写文件
ios::trunc//如果文件存在先删除再创建(清空操作)
ios::binary//二进制方式
写文件操作

写文件步骤:1.包含头文件 2.创建流对象 3.打开文件 4.写数据 5.关闭文件

#include<fstream>
std::ofstream ofs;
ofs.open("stu.txt",ios::out|ios::app);//|组合使用,打开文件并以追加方式写文件
ofs>>"dirk";
ofs.close();
读文件操作

读文件步骤:1.包含头文件 2.创建流对象 3.打开文件 4.检查是否打开 5.读数据 6.关闭文件

#include<fstream>
std::ifstream ifs;
ifs.open("stu.txt",ios::in);//|组合使用,打开文件并以追加方式写文件
if(!ifs.is_open()){
    std::cout<<"文件打开失败"<<std::endl;
    return;
}
//读文件四种方式
char buf[1024]={0};
while (ifs>>buf){
    std:::cout<<buf<<std::endl;
} 
//
char buf[1024]={0};
while (ifs.getline(buf,sizeof(buf))){
    std:::cout<<buf<<std::endl;
}
//
std::string buf;
while(getline(ifs,buf)){
    std:::cout<<buf<<std::endl;
}
//
char c;
while((c=ifs.get())!=EOF){
    std:::cout<<c<<std::endl;
}
//关闭文件
ofs.close();

二进制文件

二进制写文件主要利用流对象调用成员函数write

ofs.wirte((const char*)str ,int len);

二进制读文件主要利用流对象调用成员函数read

ofs.read((char*)&p ,siezof(Person));

C++STL技术

STL六大组件概述

  • 容器:存放数据结构的各种数据结构

    序列式容器

    关联式容器

  • 算法:各种算法

    质变算法

    非质变算法

  • 迭代器:容器与算法之间的胶合剂

    输入迭代器:只读

    输出迭代器:只写

    前向迭代器:向前推进读写

    双向迭代器:双向推进读写

    随机访问迭代器:以跳跃方式访问任何数据

  • 仿函数:行为类似函数,可作为算法的某种策略

  • 适配器:修饰容器、仿函数、迭代器接口

  • 空间配置器:负责空间的配置和管理

容器

  • 使用容器的时候,记得加上对应的头文件

  • 容器分为顺序容器和关联容器

  • 使用auto变量来保存这些函数的返回值,斌且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型

    vector<int> v{1,3,5,6,8};
    auto &i =v.back();//加上&
    i = 10;//改变容器中元素的值

选择容器的基本原则:

通常使用vector是最好的选择,除非你有很好的理由选择其他容器

如果程序中有很多小元素,且空间开销而外重要,则不要使用list或forward_list

如果程序要求随机访问,应用vector或deque

如果程序要求在中间插入或删除元素,可选择list或forward_list

如果程序只在头尾插入或删除元素,可选择deque

查找与统计选择set、map、unordered_set(map)

在以下情况使用unordered_set:仅需要保存互异的元素而不需要排序,只需要获取单个元素而不需要遍历

array容器

固定大小的数组,支持快速访问,不能添加和删除元素,也不能改变容器大小

array<int,20> arr{1,2,3};//array有固定的大小,在实例化的时候不仅要提供数据类型也要提供大小
array<int,20> arr = {1,2,3};

string容器

string是c++风格的字符串,但是本质是一个类

包含头文件#include<string>

string构造、赋值、拼接

初始化时用=为拷贝初始化,用()为直接初始化

#include<string>
//构造函数
string str1;//空字符串
string str2(const char*s);//以字符数组来初始化
string str3(const string&str);//以字符串str来初始化
string str4(int n,char c);//以n个c字符来初始话

//重载,用=来赋值
string& operator = (const char*s);
string& operator = (const string& str);
string& operator = (char c);
//用内置函数assign来赋值
string& assign(const char* s);
string& assign(const char* s,int n);
string& assign(const string& str);
string& assign(int n,char c);

//重载+=来实现拼接
string& operator+=(const char* ch);
string& operator+=(const char c);
string& operator+=(const string& str);
//用内置函数append来拼接
string& append(const char* ch);
string& append(const tring& str);
string& append(const string& str,int pos,int n);//把str中pos开始往后n个字符拼接到this字符串尾部
string 查找与替换
  • 查找:查找指定字符串是否存在

    找到返回下标

    没找到返回-1

  • 替换:在指定位置替换字符串

//从左往右查找
int find(const string& str,int pos=0)const;
int find(const char*s,int pos=0)const;
int find(const char*s,int pos,int n)const;//查找的目标是s的pos开始后n给字符的字串
int find(const char c,int pos=0)const;
//从右往左查找
int rfind(const string& str,int pos=npos)const;
int rfind(const char*s,int pos=npos)const;
int rfind(const char*s,int pos,int n)const;//查找的目标是s的pos开始后n给字符的字串
int rfind(const char c,int pos=npos)const;
//替换
string& replace(int pos,int n,const string&str);
string& replace(int pos,int n,const char*s);
string字符串比较

按照字符ASCII码进行对比

'==' 返回0

'>' 返回1

'<' 返回-1

int compare(cosnt string&str)const;
int compare(cosnt char*s)const;
string字符存取和获取子串

单个字符存取

char& operator[](int n);//重载[]来存取字符
char& at(int n);//通过at方式

获取想要的子串

string substr(int pos,int n)//返回pos开始的n个字符组成的字符串
string字插入和删除

insert有返回值,返回一个迭代器指向新加入的元素

string& insert(int pos,const char*s);
string& insert(int pos,cosnt string&str);
string& insert(int pos,int n,char c);//在pos插入n个c
string& erase(int pos,int n);//删除pos开始的n个字符

str.append();//在字符串末尾加上
str.replace(pos,n,elem)//从pos开始删除n个字符并插入elem
string其他常用操作
str.empty();//空返回true,非空返回false
str.size();//返回字符的个数
os<<str;//str写到输出流os中,返回os
is>>str;//将is中读取的字符赋给str,字符串以空白分隔,返回is
getline(is,str);//从is中读取一行赋给str,返回is
处理string对象中的字符

我们经常要单独处理string对象中的字符,比如检查一个string对象是否包含空白等等。

cctype头文件中定义了一组标准库函数处理这部分工作

#include<cctype>
isalnum(c)	//如果参数为字母或数字,返回true
isalpha(c)	//如果参数为字母,返回true
isblank(c)	//如果参数为空格(包括TAB键和space键输入),返回true
iscntrl(c)	//如果参数为控制字符,返回true,控制字符不会打印显示,可用isprint()判断
isdigit(c)	//如果参数为数字,返回true
isgraph(c)	//如果参数是除空格之外的打印字符,返回true
islower(c)	//如果参数为小写字母,返回true
isprint(c)	//如果参数为打印字符,返回true
ispunct(c)	//如果参数为标点符号,返回true
isspace(c)	//如果参数为空白符,如回车、空格、Tab…,返回true
isupper(c)	//如果参数为大写字母,返回true
isxdigit(c)	//如果参数为十六进制数,返回true
tolower(c)  //如果c是大写则输出对应小写
toupper(c)  //如果c是小写则输出对应大写

基于范围的for语句

  • 遍历字符串中每个字符,可以配合上面标准库函数进行各种字符计数或其他功能

    string str;
    //遍历输出每个字符后换行
    for(auto c:str){
        cout<<c<<endl;
    }

  • 使用for语句改变字符串中的字符

    #include<cctype>
    ...
    for(auto &c : str){
    	c=toupper(c);
    }

vector容器

vector基本概念

vector数据结构和数组非常类似,也称单端数组

vector的迭代器是支持随机访问的迭代器

vector与普通数组的区别:

数组:静态空间

vector:动态扩展-找更大的空间拷贝过去释放原空间

vector构造与赋值

初始化如果提供列表的值用{},用()含义完全不一样

vector<int> v1;
vector<int> v2(v1.begin(),v1.end());
//注意下面两种情况
vector<int> v3(20,2);//列表有20个值 为20个2
vector<int> v4{20,2}//列表只有两个值分别是20和2
vector<int> v5(v1);//拷贝构造
//赋值
vector& operator=(const vector<T> &v);//重载运算符=
v4.assign(v1.begin(),v.end());
v4.assign(20,2);//20个2
vector容量和大小操作

只有当迫不得已的时候才可以分配新的空间

vector<int> v;
v.empty();//是否为空
v.size();//元素个数
v.capacity();//容量大小
v.resize(int num);//重新指定容器长度,超出部分用默认值0填充,如果减少就删除后部分
v.resize(int num,elem);//以elem值填充
v.reserve(int num)//预先分配num大的空间
vector插入和删除

不可用下标的模式来添加元素

insert有返回值,返回一个迭代器指向新加入的元素

vector<int> v;
v.push_back(elem);//尾部插入一个元素
v.pop_back();//删除最后一个元素
v.insert(pos,elem);
v.insert(pos,int n,elem);
v.erase(pos);//删除pos位置元素
v.erase(beg,end);//删除区间元素
v.clear();//删除全部元素
​
v.empalce(elem);//创建一个元素,而不是拷贝

emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配

vector数据存取
v.at(int index)//at(索引下标)来返回对应元素
vector& operator[](int n);//重载[]符号来实现v[index]返回对应元素
v.front();//返回第一个元素
v.back();//返回最后一个元素
vector互换容器与预留空间

预留语句预留的位置不初始化所以预留位置的元素不可访问

v1.swap(v2);//v1 v2进行元素互换
v.reserve(int len);//预留len个元素位置

deque容器

deque基本概念

deque为双端数组,可以对头端和尾端进行插入和删除操作

deque与vector的区别:

vector对于头部的删除和插入效率低,数据量越大,效率越低

deque相对而言,对头部的插入和删除操作速度比vector快

vector访问元素时的速度比deque快,这和两者的内部实现有关

deque内部工作原理:deque内部有一个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据,中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续内存空间

deque构造与赋值
deque<int> d1;
deque<int> d2(d1.begin(),d1.end());
deque<int> d3(20,2);//20个2
deque<int> d4(d1);//拷贝构造
//赋值
deque& operator=(const vector<T> &d);//重载运算符=
d4.deque(d1.begin(),d1.end());
d4.assign(20,2);//20个2
deque容量和大小操作
deque<int> d;
d.empty();//是否为空
d.size();//元素个数
d.resize(int num);//重新指定容器长度,超出部分用默认值0填充,如果减少就删除后部分
d.resize(int num,elem);//以elem值填充
deque插入和删除

insert有返回值,返回一个迭代器指向新加入的元素

deque<int> d;
d.push_back(elem);//尾部插入一个元素
d.pop_back();//删除最后一个元素
d.push_front(elem);//头部插入一个元素
d.pop_front(elem);//删除第一个元素

d.insert(pos,elem);
d.insert(pos,int n,elem);
d.insert(pos,beg,end);//在pos位置插入beg到end区间的元素
d.erase(pos);//删除pos位置元素
d.erase(beg,end);//删除区间元素
d.clear();//删除全部元素
deque数据存取
d.at(int index);//at(索引下标)来返回对应元素
deque& operator[](int n);//重载[]符号来实现d[index]返回对应元素
d.front();//返回第一个元素
d.back();//返回最后一个元素
deque的排序
bool mycompare(int num1,int num2){
    return num1>num2;
}
d.sort(mycompare);//降序 mycompare是自定义的函数(排序逻辑)

list容器

list基本概念

list将数据进行链式存储,链表由一系列结点组成,结点有数据域和指针域

STL中链表是一个双向循环链表,list的迭代器只支持前移和后移,属于双向迭代器,不支持随机访问

  • list:双向链表,只支持双向顺序访问,在链表任何位置进行插入/删除操作都很快

  • forward_list:单向链表,只支持单向顺序访问,在链表任何位置进行插入/删除操作都很快。为了节省开销,forward_list没有size操作

list优缺点:

优点:采用动态存储分配,不会造成浪费或溢出。链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素

缺点:遍历消耗大

vector和list是最常用的两个容器,各有优缺点

list构造、赋值、交换
list<T> lst;
list(beg,end);
list(n,elem);
list(const list &lst);

lst.assign(beg,end);
lst.assign(n,elem);
list& operator=(const list &lst);

lst1.swap(lst2);
list大小操作
l.empty();
l.size();
l.resize(num);//重新指定list长度,用0填充
l.resize(num,elem);//重新指定list长度,用elem填充
list插入、删除

insert有返回值,返回一个迭代器指向新加入的元素

l.push_back();
l.pop_back();
l.push_front();
l.pop_front();

l.insert(pos,n,elem);
l.insert(beg,beg,end);

l.clear();
l.erase(beg,end);//返回下一个数据位置
l.erase(pos);//返回下一个数据位置
l.remove(elem)//删除容器中所有与elem匹配的项
list数据存取
l.front();//返回第一个元素
l.back();//返回最后一个元素
list反转和排序
l.reverse();//反转
l.sort();//排序
l.sort(mycompare);//降序 mycompare是自定义的函数(排序逻辑)
list案例
class person
{
public:
	person(string name, int age, int higher)
	{
		this->p_name = name;
		this->p_age = age;
		this->p_higher = higher;
	}
	string p_name;
	int p_age;
	int p_higher;
};
bool compareperson(person& p1, person& p2)//定义排序的规则!!!
{
	if (p1.p_age == p2.p_age)//年龄相同情况,按照升高降序
	{
		return p1.p_higher > p2.p_higher;//降序
	}
	else//年龄不同按照年龄升序
	{
		return p1.p_age < p2.p_age;//升序
	}
}



void myprint(person& p)
{
	cout << p.p_name<<p.p_age<<p.p_higher << endl;
}
void printlist(list<person>& l)
{
	for_each(l.begin(), l.end(), myprint);
}

int main()
{ 
	person p1 = { "zhangsan",21,210 };
	person p2 = { "xiaoli",20,190 };
	person p3 = { "wkao", 20,200 };
	list<person>L;
	L.push_back(p1);
	L.push_back(p2);
	L.push_back(p3);
	L.sort(compareperson);
	printlist(L);

	system("pause");
	return 0;
}

forward_list容器

forward_list是一个单向链表,且这个容器被设计出来是为了减少一些消耗,所以forward_list容器内部定义与其他容器有较大差别

forward_list没有定义insert、emplace、erase,而是定义了insert_after、emplace_after、erase_after,列如要删除一个元素,则使用指向这个元素前一个元素的迭代器调用erase_after来删除这个元素

lst.before_begin();//返回指向链表首元素之前不存在的元素的迭代器,这个迭代器不可解引用
lst.cbefore_begin();//返回的是const_iterator

lst.insert_after(pos,elem);//在pos之后的那个位置加上elem
lst.insert_after(pos,n,elem);

lst.erase_after(pos);//删除pos之后的元素
lst.erase_after(beg,end);//删除beg之后到end之间的元素(不包括end和beg)

set/multiset容器

set/multiset概念
  • set/multiset属于关联式容器,底层结构用二叉树实现

  • 存入容器的元素会按照值的大小升序排列

  • set与multiset的区别:

    set不允许容器中有重复元素,而multiset允许有重复元素

    set插入数据的同时会返回插入的结果,表示是否成功

    multiset不会检测数据,因此可以插入重复数据

set/multiset接口操作
//构造与赋值
set<int> st;
set (const set &st);
set& operator=(const set &st);

//大小和交换操作
st.size();
st.empty();
st2.swap(st1);

//插入和删除
st.insert(elem);
st.clear();
st.erase(pos);
st.erase(beg,end);
st.erase(elem);

//查找与统计
st.find(key);//key存在返回该元素迭代器,不存在返回st.end()
st.count(key);//返回一个int型
set的insert操作返回值

set的insert操作返回值是一个pair对组pair<set<int>::iterator,bool>

pair<set<int>::iterator,bool> ret = st.insert(10);
if(ret.second){
    cout<<"success"<<endl;
}
else{
	cour<<"failed"<<endl;    
}

而multiset的insert操作返回的是一个迭代器,没有bool类型

set容器排序
存放内置数据类型

存放内置数据类型set默认规则为升序,可以自定义一个重载()的且返回值是bool类型的类—也称为谓词,在创建set的时候加上这个类就可以实现改变规则

set<int,mycompare> st;其中mycompare就是定义的规则

class mycompare{
public:
    bool operator()(int val1,int val2){
    	return val1 > val2;//降序
    }
}
...
set<int,mycompare> st;//st排序规则已经改为降序
存放自定义数据类型

set存放自定义数据类型的时候必须要提供排序规则,否则无法成功插入

class Person{
public:
    int p_name;
    int p_age; 
}
class Mycompare{
public:
    bool operator()(Person &p1,Person &p2){
    	return p1.p_age < p2.p_age;//按年龄升序排列
    }
}
...
set<Person,Mycompare> st;

map/multimap容器

map/multimap概念
  • map/multimap属于关联式容器,底层由二叉树实现

  • 存入容器的元素都是pair对组,pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值),所有元素都会根据元素的键值自动排序

  • 可以根据key值快速找到value值

  • map/multimap的区别:

    map不允许容器中有重复key值元素,而multimap允许有重复key值元素

    set插入数据的同时会返回插入的结果,表示是否成功

    multiset不会检测数据,因此可以插入重复数据

set/multiset接口操作
//构造与赋值
map<T1,T2> mp;
map (const map &mp);
map& operator=(const map &mp);

//大小和交换操作
mp.size();
mp.empty();
mp2.swap(mp1);

//插入和删除
mp.insert(elem);
>//插入四种方式
>mp.insert(pair<int,int>(1,10));
>mp.insert(make_pair(1,10));
>mp.insert(map<int,int>::value_type(1,10));
>mp[1]=10;//[]虽然可以用于插入,但是不建议,建议只用[]来访问 
mp.clear();
mp.erase(pos);
mp.erase(beg,end);
mp.erase(key);

//查找与统计
mp.find(key);//key存在返回该元素迭代器,不存在返回st.end()
mp.count(key);//返回一个int型
map容器排序
  • 对于key为内置数据类型,默认规则为按照key值进行从小到大升序排序

  • 对于key为自定义数据类型,必须提供排序规则

class Person{
public:
    int p_name;
    int p_age; 
}
class Mycompare{
public:
    bool operator()(Person &p1,Person &p2){
    	return p1.p_age < p2.p_age;//按年龄升序排列
    }
}
...
map<Person,int,Mycompare> mp;
map/multimap案例
//员工分组--案例需求
//1. 创建10名员工,放到vector中
//2. 遍历vector容器,取出每个员工,进行随机分组
//3. 分组后,将员工部门编号作为key,具体员工作为value,放入到multimap容器中
//4. 分部门显示员工信息
​
class employee
{
public:
    employee(string name, int salary)
    {
        this->e_name = name;
        this->e_salary = salary;
    }
    string e_name;
    int e_salary;
};
void createworker(vector<employee>& v)
{
    string nameseed = "ABCDEFGHIJ";//分配员工姓名,用string []访问字符串中各个字符然后分配较为方便
    for (int i = 0; i < 10; i++)
    {
        string name;
        int salary = 0;
        name = nameseed[i];
        salary = rand() % 10000 + 10000;//随机分配工资(10000以内的随机数 0~9999 加上10000)
        employee e(name, salary);
        v.push_back(e);
    }
}
void setgroup(vector<employee>& v, multimap<int, employee>& m)
{
    for (vector<employee>::iterator it = v.begin(); it != v.end(); it++)
    {
        int depid = rand() % 3;//随机分组,产生一个0-2的随机数
        m.insert(make_pair(depid, *it));//员工和部门编号插入到multimap容器zhon
    }
}
void showempbygroup(multimap<int, employee>& m,int select)
{
    for (map<int, employee>::iterator it = m.begin(); it != m.end(); it++)
    {
        if (it->first == select)
        {
            cout << it->second.e_name << " " << it->second.e_salary << endl;
        }
    }
}
​
int main()
{
    vector<employee> V;
    createworker(V);
    multimap<int, employee> M;
    setgroup(V, M);
    cout << "新入职员工信息如下:" << endl;
    cout << "策划部门:" << endl;
    showempbygroup(M, 0);
    cout << "美术部门:" << endl;
    showempbygroup(M, 1);
    cout << "研发部门:" << endl;
    showempbygroup(M, 2);
    system("pause");
    return 0;
}

unordered_set&&unordered_map

  • set和map内部实现是基于RB-Tree,而unordered_set和unordered_map内部实现是基于哈希表

  • unordered_set 容器类型的模板定义在 <unordered_set> 头文件中。

  • unordered_set 容器提供了和 unordered_map 相似的能力,但 unordered_set 可以用保存的元素作为它们自己的键。T 类型的对象在容器中的位置由它们的哈希值决定,因而需要定义一个 Hash< T > 函数。基本类型可以省去Hash< T >方法。

  • 不能存放重复元素。

  • 可指定buckets个数,可进行初始化,也可后期插入元素

  • 在以下情况使用unordered_set:仅需要保存互异的元素而不需要排序,只需要获取单个元素而不需要遍历

算法

头文件

  • < algorithm >最大的一个,涉及很多运算

  • < functional >定义了一些模板类,可以用声明函数对象(仿函数)

  • < numeric >体积很小,只包括几个在序列上面进行简单数学运算的模板函数

常用遍历算法

  • for_each遍历容器:

    _func():

    如果用的是仿函数就加()

    如果用的是函数则只需要函数名

    for_each(beg,end,_func())
  • transform遍历搬运算法:

    遍历beg1到end1,func由返回值给到beg2开始容器

    目标容器需要reserve一下大小

    _func():

    如果用的是仿函数就加()

    如果用的是函数则只需要函数名

    transform(beg1,end1,beg2,_func())

常用查找算法

  • find

    按值查找,找到返回迭代器,找不到返回结束迭代器

    find(iterator beg,iterator end,value)
  • find_if

    通过_ Pred来提供条件查找策略,_Pred为函数或者谓词

    find(iterator beg,iterator end,_Pred)
  • adjacent_find

    查找相邻重复元素,返回相邻元素的第一个位置的迭代器,未找到返回end

    adjacent_find(iterator beg,iterator end)
  • binary_search

    返回值是bool类型的

    在无序序列中不可用,只可用于有序序列

    binary_serach(beg,end.value)
  • count

    count(beg,end,value)
  • count_if

    通过_ Pred来提供条件计数策略,_Pred为函数或者谓词

    count(beg,end,_Pred)

常用的排序算法

  • sort排序

    通过_ Pred来提供条件计数策略,_Pred为函数或者谓词

    sort(beg,end,_Pred)
    sort(v.begin(),v.end(),greater<int>());//greater是一个内建谓词
  • random_shuffle洗牌

    范围内元素随机调整次序

    #include<ctime>//随机种子的头文件
    srand((unsigned int)time(NULL));//为了防止每次一样,加一个随机种子
    random_shuffle(beg,end);
  • merge合并

    两个容器元素合并,并存储到另一个容器中

    目标容器需要提前reserve一下所需空间

    merge(beg1,end1,beg2,end2,dest)//dest为目标容器起始迭代器
  • reverse反转

    reverse(beg,end)

常用拷贝函数和替换函数

  • copy复制

    把一个范围复制到另一个容器当中

    目标容器需要提前reserve一下所需空间

    copy(beg,end,dest)//dest为目标容器起始迭代器
  • replace替换

    将容器内指定范围的符合条件的旧元素改为新元素

    replace(beg,end,oldvalue,newvalue)//条件为==oldvalue
  • replace_if条件替换

    通过_ Pred来提供条件计数策略,_Pred为函数或者谓词

    replace(beg,end,_Pred,newvalue)//条件为_Pred
  • swap互换

    swap(container c1,container c2)//两个容器需要同种类型

常用算术生成算法

< numeric >

  • accumulate累加

    accumulate(beg,end,value)//结果为 value + 各个元素的累加
  • fill添加指定元素(填充)

    一个空容器用resize了一个空间默认填充为0,如果后期重新填充则用fill

    fill(beg,end,value)//value为填充的值

常用集合算法

  • set_intersection求两个集合的交集

    两个集合必须有序

    目标容器开辟空间从两个集合大小中选较小值

    返回值为交集容器中最后一个元素的位置

    set_intersection(beg1,end1,beg2,end2,dest)
  • set_union求两个集合的并集

    两个集合必须有序

    目标容器开辟空间大小为两个集合大小之和

    返回值为并集容器中最后一个元素的位置

    set_union(beg1,end1,beg2,end2,dest)
  • set_difference求两个集合的差集

    两个集合必须有序

    目标容器开辟空间从两个集合大小中选较大值

    返回值为差集容器中最后一个元素的位置

    set_difference(beg1,end1,beg2,end2,dest)

迭代器

  • 迭代器使算法不依赖于容器

    vector<int> v;
    v.sort();//依赖容器
    
    sort(v.begin(),v.end());//不依赖容器

迭代器范围

一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这两个迭代器通常被称为beg和end,它们标记了容器中元素的一个范围

迭代器范围在两个迭代器之间是左闭右开,也称为左闭合区间

如果beg=end,则范围为空

如果beg!=end,则范围内至少有一个元素且beg指向第一个元素

可以对beg递增若干次使beg==end

迭代器失效

  • 向容器添加元素后:

    如果容器为vector和string,存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效

    对于deque,插入到除了首尾位置之外的任何位置都会导致迭代器、指针和引用失效

    对于list和forward_list,指向容器的迭代器、指针和引用仍有效

  • 删除一个元素后:

    对于list和forward_list,指向容器其他位置的迭代器、指针和引用仍有效

    对于deque,删除的是除了首尾位置之外的任何位置都会导致迭代器、指针和引用失效。删除的是尾迭代器,只有尾后迭代器失效

    对于vector和string,指向被删元素之前的迭代器,指针和引用都有效。尾后迭代器总会失效

由于添加元素和删除元素都有可能会导致迭代器失效,所以必须保证每次改变容器的操作之后都正确地重新定位迭代器

对于insert和erase操作,更新迭代器很容易,因为这两个操作都有返回值,会返回操作后的新迭代器

因为在vector、string和deque中增删元素后,原来end返回的迭代器总会失效,所以不能在循环前保存end返回的迭代器,要在循环中反复调用end来更新

仿函数

概念

  • 重载函数调用符的类,其对象常称为函数对象

  • 函数对象使用重载的()时,行为类似函数调用,也叫仿函数

  • 实质是一个类,不是一个函数

仿函数的调用

  • 函数对象在调用时,可以像普通函数那样调用,可以有参数,可以有返回值

  • 函数对象超出函数的概念,函数对象可以有自己的状态(类的属性)

  • 函数对象可以做为参数传递

    有返回值时可以做参数传递

    以对象做为参数传递

谓词

  • 返回bool类型的仿函数称为谓词

  • 如果operator()接受一个参数,叫做一元谓词

  • 如果operator()接受两个参数,叫做二元谓词

  • 可以利用谓词来改变和提供排序规则

    class MyCompare{};
    sort(beg,end,MyCompare());
    set<int,int,MyCompare>;

内建仿函数

  • 分类:

    算术仿函数

    关系仿函数

    逻辑仿函数

  • 用法:仿函数所产生的函数对象,用法与一般函数完全相同

  • 使用内建函数对象,需要引入头文件< functional >

  • 内置仿函数只能应用于内置数据类型

算术仿函数
temlate<class T> T * * * <T>
//二元运算
plus<int>(a,b);//a+b
minus<int>(a,b);//a-b
multiplies<int>(a,b);//a*b
divides<int>(a,b);//a/b
modulus<int>(a,b);//a%b
//一元运算
negate<int>(a);//-a取反(相反数)
关系仿函数

也就是内建的谓词

equal_to<int>(a,b);//a==b返回true
no_equal_to<int>(a,b);//a!=b返回true
greater<int>(a,b);//a>b返回true
greater_equal<int>(a,b);//a>=b返回true
less<int>(a,b);//a<b返回true
less_equal<int>(a,b);//a<=b返回true
逻辑仿函数
logical_and<bool>(a,b);//逻辑与运算
logical_or<bool>(a,b);//逻辑或运算
logical_not<bool>(a);//逻辑非运算

适配器

  • 适配器是标准库里一个通用的概念,适配器在STL中扮演着转换器的角色,本质上是一种设计模式,用于将一种接口转换成另一种接口,从而是原本不兼容的接口能够很好地一起运作

  • 容器、迭代器、和函数都有适配器, 但适配器不提供迭代器。

  • 使用适配器也需要加上对应头文件

容器适配器

容器适配器是一个封装了序列容器的一个类模板,它在一般的序列容器的基础上提供了一些不同的功能。

虽然适配器也可以存储元素,但是不叫它们容器。

之所以称为容器适配器,是因为它是适配容器来提供其它不一样的功能。

stack容器适配器
stack基本概念

stack是栈,先进后出,只有一个出口,栈顶进,栈顶出

栈只有顶端元素才可以被访问利用,因此栈不允许有遍历行为

stack常用接口
//构造
stack<T> stk1;
stack<T> stk2(const stack &stk1);
//赋值
stack&operator=(cosnt stack &stk);
//存取
s.push(elem);
s.pop();
s.top();
//大小
s.empty();
s.size();
queue容器适配器
queue基本概念

queue是队列,先进先出,只有一个出口和一个入口,队尾进,队头出

队列只有队头队尾两端元素才可以被访问利用,因此队列也不允许有遍历行为

queue常用接口
//构造
queue<T> que1;
queue<T> que2(const queue &que1);
//赋值
queue&operator=(cosnt queue &que);
//存取
q.push(elem);//队尾加入一个元素
q.pop();//队头删除一个元素
q.back();//返回队尾元素
q.front();//返回队头元素
//大小
q.empty();
q.size();

queue与stack容器不允许有遍历行为,赋值也只有一种方法—同容器赋值

pirority_queue容器适配器
pirority_queue基本概念

pirority_queue是优先队列,为队列中的元素建立优先级,新加入的元素排在优先级比它低的已有元素之前

队列只有队头队尾两端元素才可以被访问利用,因此优先队列也不允许有遍历行为

pirority_queue常用接口
//构造
pirority_queue<T> pq1;
pirority_queue<T> pq2(const pirority_queue &pq1);
//赋值
pirority_queue&operator=(cosnt pirority_queue &pq);
//存取
pq.push(elem);//队尾加入一个元素
pq.pop();//队头删除一个元素
pq.back();//返回队尾元素
pq.front();//返回队头元素
//大小
pq.empty();
pq.size();

空间配置器

C++提高编程

Template

模板

模板只会在调用的时候创建,不被调用的时候不会发生编译错误的,有错误也不会报出

模板的作用是减少代码重复,使代码更加简洁

使用模板可以将数据类型等等作为变量

#include<iostream>
template<typename T, int N>
class Array//模板类-数组
{
public:
    T m_Array[N];
}
int main
{
    Array<int,20> arr;//创建了一个大小为20的整型数组
}

类模板和函数模板

  • 类模板:需要指定好数据类型

    类模板中的成员函数在调用时才会创建

    类模板继承到父类是一个类模板时,子类声明的时候需要指出父类中的T的类型,不然编译器无法给子类分配内存

    如果子类想灵活的指出父类中T的类型,子类也需变成类模板

  • 类模板的分文件编写:如果分为.h和.cpp文件的话,由于成员函数在运行才构建,只包含.h文件导致编译器看不见.cpp的内容会出错

    解决方案1:程序头文件中包含.cpp文件

    解决方案2:将.h和.cpp文件内容写到一起,后缀名改为.hpp文件

  • 函数模板:函数调用可以发生自动类型转换

    函数模板在针对自定义数据类型可能无法有效,所以需要函数模板重载

Template案例

template<typename T>
void selectsort(T *a,int len)//选择排序模板
{
	for (int i = 0; i < len-1; i++)//每轮确定一个最大的数
	{
		int index = i;
		for (int j = i+1; j < len; j++)//内层循环比较
		{
			if (a[index] <= a[j])
			{
				index = j;//使index始终为此轮排序目前最大值的下标
			}
		}
		//交换目前操作序列第一位与得到的最大元素的位置
		int temp = a[i];
		a[i] = a[index];
		a[index] = temp;
	}
}

int main()
{
	
	int a[] = {1,2,3,4,5};
	char carr[] = "abcde";//字符数组可以排序,编译器按照每个字符对应的ASCII码比较大小!!!
	int len = sizeof(carr)/sizeof(char);
	selectsort<char>(carr,len);
	
	for (int i = 0; i < len; i++)
	{
		cout << carr[i] << endl;
	}
	return 0;
}

随机数生成

加头文件ctime

#include<ctime>//头文件ctime
int main()
{
    std::srand((unsigned int) time (NULL));//随机种子,生成的随机数随时间变化
    std::cout << rand ()%100 + 1;//生成一个1-100的数字
}

库-动态链接&静态链接

静态链接:

链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的一个函数,那么链接器就会把库中包含这个函数的那个目标文件链接进来,生成一个可执行文件.exe。

  • 静态链接的缺点:一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了某一个函数,则这多个程序中都含有头文件,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

  • 静态链接的优点:在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

动态链接:

动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

动态链接的优点:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

动态链接的缺点:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

隐式转换与explicit关键字

一个类构造函数参数只有其中一种属性时,构造这个类时用=调用这个构造参数会触发隐式类型转换

构造函数前面加上explicit关键字了就只能用显示转换

class Person
{
private:
    std::string m_Name;
    int m_Age;
public:
    Person(std::string name)
    	:m_Name(name),m_Age(-1){};
    explicit person(int age)
        :m_Name("unknown"),m_Age(age){};
}
int main
{
    Person p1("dirk");
    //下面两种是隐式类型转换,虽然Person由两种属性,但是还是可以=string或者int,由于存在相应的构造函数,所以可以触发隐式类型转换。 但是这种表达不建议使用,建议用上面那种,可读性较强。
    person p2 = "dirk";
    //person p3 = 24;这是错误的,因为上面构造函数用了explicit关键字,所以不能再使用隐式类型转换
}

New

new关键字用于在堆区开辟内存,堆区开辟的内存就不会像在栈区中作用域过了就自动释放,所以一旦使用new在堆区开辟内存就要考虑在使用完用delete关键字释放这片内存

person * p = new person();new关键字会返回一个指向堆区内存的指针

delete p;删除这个指针就释放了堆区内存

int *p = new int(30);
delete p;

person* p =new person(); 一定程度上可以看成 person* p = (person*) malloc(sizeof(person));但是前者调用了构造函数而后者并没有调用构造函数,所以不能在C++中用后面这种方式分配内存

函数指针

指向函数的指针,可以在一些情况下调用函数

void helloWorld()
{
    std::cout<<"hello world"<<std::endl;
}
void (*func)() = helloWorld;//函数名,func是指向这个函数的指针
func();//相当于调用函数helloWorld();

快速的一次性函数lambda

auto lambda = [](int value){std::cout<<"Value:"<<value<<std::endl;}

[] 表示capture捕获, [&],[=]表示引用传递和值传递

() 内表示参数

{} 内是函数主体

int value = 20;
for_each(v.begin(),v.end(),[](int val){std::cout << val << std::endl;});//打印容器v中的元素
//()内的第三部分就是一个lambda一次性函数

智能指针

内存控制不当会导致一下问题:

  • 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;

  • 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);

  • 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。

作用域指针unique_ptr:

unique_ptr在超出作用域时会自动调用delete来释放内存

{}中间就是一个作用域

头文件#include<memory>

#include<memory>
{
std::unique_ptr<person> p (new person());
}
//p为指针名,在{}作用域中创建的作用域指针p,程序执行到}会自动释放堆区的空间

共享指针shared_ptr:

每个 shared_ptr 对象在内部指向两个内存位置:

  • 指向对象的指针;

  • 用于控制引用计数数据的指针。

shared_ptr工作:

1.当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。

2.当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。

// 最好使用make_shared创建共享指针
shared_ptr<int> p1 = make_shared<int>();//make_shared 创建空对象,
​
shared_ptr<T> a(new int());
shared_ptr<T> b(new int());
a = b;  // 此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1
        //shared_ptr 也可以直接赋值,但是必须是赋给相同类型的 shared_ptr 对象,而不能是普通的 C 指针或 new 运算符的返回值。
        //当共享指针 a 被赋值成 b 的时候,如果 a 原来是 NULL, 那么直接让 a 等于 b 并且让它们指向的东西的引用计数加 1;
        // 如果 a 原来也指向某些东西的时候,如果 a 被赋值成 b, 那么原来 a 指向的东西的引用计数被减 1, 而新指向的对象的引用计数加 1。
​
//共享指针函数功能
shared_ptr<T> ptr(new int(1024));//直接初始化格式
//shared_ptr<T> ptr = new int(1024);错误的,必须使用初始化格式
*ptr = 10;
ptr.reset(new T()); // 原来所指的对象会被销毁,且ptr被置为NULL
std::cout<<ptr.count()<<std::endl;//打印引用个数
​
​

不要混合使用普通指针和智能指针,使用一个内置指针来访问一个智能指针所负责的对象是很危险的,我们无法知道对象什么时候销毁

“宏”

#define NAME "Dirk"
#define STUDENTFILE "student.txt" 
//可以使用宏定义文件名、类的属性、函数名...

不要随意使用宏定义,要尽量写真实代码,随意使用宏定义是一件很蠢的事情,让别人很难读懂你的代码

Namespace

C++有命名空间概念和功能,而C并没有。命名空间主要是解决命名冲突,包括函数名,类名等等

stl标准库中的所有东西的名字都在命名空间std中,所有要用标准库中的东西需要加上std::

不建议在头文件部分中使用using namespace std; 因为在大的项目中会有很多的库,我们需要加上前缀来分清这个类或函数来自于哪个库,这样会提高代码可读性

线程控制

多线程并发可以提高效率,线程控制用于程序优化

加头文件#include< thread >

#include<iostream>
#include<thread>
static bool is_exist = false;
void print()
{
	using namespace std::literals::chrono_literals;//特殊命名空间(包含sleep_for)
	std::cout << std::this_thread::get_id() << std::endl;//获取这个线程名字
	while(!is_exist)
	{
		std::cout << "working...\n";
		std::this_thread::sleep_for(0.5s);//每0.5s打印一次
	}
}

void test_01()
{
	std::thread worker(print);//用print是一个函数指针,线程语句调用函数
	std::cin.get();//输入来打断程序
	is_exist = true;
	worker.join();//线程加入:完成后继续进行主线程任务
}

int main()
{
	test_01();
	system("pause");
	return 0;
}

时间计时

计时功能有助于我们判别代码的效率和对程序进行优化

加头文件#include<chrono> 可以设计一个计时类,在构造函数中记录开始时间,在析构函数中记录结束时间并计算和输出总耗时

#include<thread>//线程库
#include<chrono>//计时库
#include<iostream>
class Timer//自定义计时类
{
public:
    std::chrono::time_point<std::chrono::steady_clock> start, end;//定义开始结束时间属性(属性的类型)
    std::chrono::duration<float> duration;
    Timer() 
    {
        start = std::chrono::high_resolution_clock::now();//开始时间
    }
    ~Timer()
    {
        end = std::chrono::high_resolution_clock::now();//结束时间
        duration = end - start;//计时
        float ms = duration.count() * 1000.0f;
        std::cout << "总耗时:" << ms << 'ms' << std::endl;//输出计时
    }
};
static bool is_exist = false;
void print()
{
	using namespace std::literals::chrono_literals;//命名空间
	std::cout << std::this_thread::get_id() << std::endl;//获取这个线程名字
	while(!is_exist)
	{
		std::cout << "working...\n";
		std::this_thread::sleep_for(0.5s);//每0.5s打印一次
	}
}

void test_01()
{
	std::thread worker(print);
	std::cin.get();
	is_exist = true;
	worker.join();//这个完成后继续进行主线程任务

}

int main()
{
    Timer timer;//在主函数中先创建一个计时类,就可以自动并输出记录运行总耗时
	test_01();
	system("pause");
	return 0;
}

多维数组

二维数组:一个一维数组里面每个元素存着是其他一维数组的指针

一个int型大小为4B,所以new int[20]大小为80B,一个指针大小为4字节,所以一个new int*[20]大小也为80B

一般少用2d数组,因为比较复杂,2d数组在特定场合下用(如:存储一张图片的像素)

#include<iostream>
int main()
{
    int* arr = new int[50];
    int** a2d = new int*[50];
    for(int i=0;i<50,i++)//赋值
    {
        awd[i]=mew int[50];
    }
    std::cout<<awd[1][1]<<std::endl;//访问二维数组
    //delete[] a2d;这个是错误的,不可以这样释放堆区的空间
    for(int i=0;i<50;i++)//要通过遍历来释放堆区的空间!!!
    {
     	delete[] awd[i];   
    }
}

联合体

union即为联合,它是一种特殊的类。通过关键字union进行定义,一个union可以有多个数据成员。

union Token{
   char cval;
   int ival;
   double dval;
};

互斥赋值:在任意时刻,联合中只能有一个数据成员可以有值。当给联合中某个成员赋值之后,该联合中的其它成员就变成未定义状态---->>所以联合体的大小和最大的成员大小一样

类型转换

自动类型转换:

当不同类型的变量同时运算时就会发生数据类型的自动转换:

  • char 和 int 两个类型的变量相加时,就会把 char 先转换成 int 再进行加法运2

  • 如果是 int 和 double 类型进行运算时,就会把 int 转换成 double 再进行运算。(高位补0)

  • 条件判断中,非布尔型自动转换为布尔类型。

用一个参数作为另一个不同类型参数的赋值时出现的自动转换:

  • 当定义参数是char,输入是int时,自动将int通过ASCII转换为字符

  • 当定义参数是int,输入是浮点型,自动转换为浮点型。

C风格强制类型转换:

int a = (int)"sss";
string r = (string)23;
float f = 34;
int b = 5.23;//会丢失数据部分导致出错
string st;
char* ch = (char*)st;
char* cn = (char*)345;

C++风格类型转换:

用法:cast_name<type>(expression); type是目标类型,expression是被转换的值,cast_name有下面四种:

  • static_cast:不提供运行时的检查,在编写时需要自己确认转换的安全性,而且不能转换底层的const,volatile,_unaligned属性。

  • dynamic_cast:会在运行时检查类型转换的合法性,所以一般用于验证,具有一定的安全性,但是会产生一些额外的消耗。

    dynamic_cast一般通过返回值验证继承方面,基类转换衍生类或者衍生类转换基类。

    父类指针 = 子类指针 是安全的,但是 子类指针 = 父类指针是不安全的,不知道这个父类指针指向到底是什么子类

    dynamic转换仅适用于指针或者引用,若指针转换失败,则返回空指针NULL,若引用转换失败,则抛出异常。

  • const_cast: 用于移除const,volatile,_unaligned属性,常量指针被转换为非常量指针,并且仍然指向原来的对象,常量引用被转换成非常量引用,并且仍然引用原来的对象。

  • reinterpret_cast: 非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全,非极端情况不要使用

预编译头文件

当项目越来越大时,每个.cpp文件都会包含很多的头文件,这样每次做出修改后编译的时候都会对头文件进行处理,大大增加了编译时间消耗。使用预编译头文件可以使所需要的头文件只编译一次就以二进制存储下来,之后修改等都不需要再对头文件进行解析,减少编译的时间消耗。

  1. 构建一个pch.h和一个pch.cpp文件,pch.h文件包含所用的所有头文件,pch.cpp加上#include<pch.h>

  2. pch.cpp文件属性>>C++>>预编译头>>是否创建 改成创建预编译头文件

  3. 整个项目属性>>C++>>预编译头>>是否使用 改成使用,下面文件改成pch.h

//pch.h头文件(预编译)
#pragma once
//输入输出
#include<iostream>
//文件处理
#include<fstream>
//容器
#include<vector>
#include<deque>
#include<list>
#include<string>
#include<array>
#include<map>
#include<set>
//算法
#include<algorithm>
#include<functional>
//智能指针
#include<memory>
//计时
#include<chrono>
//线程
#include<thread>

基准测试

实际测试C++代码的性能,基准测试没有标准答案,每个人都有自己的评判标准来衡量性能。需要小心处理,衡量性能本身会增加开销

可以使用时间计时的内容案例建一个Timer类来计时

#include<thread>//线程库
#include<chrono>//计时库
#include<iostream>
class Timer//计时类
{
public:
    std::chrono::time_point<std::chrono::steady_clock> start, end;//定义开始结束时间属性(属性的类型)
    std::chrono::duration<float> duration;
    Timer() 
    {
        start = std::chrono::high_resolution_clock::now();//开始时间
    }
    ~Timer()
    {
        end = std::chrono::high_resolution_clock::now();//结束时间
        duration = end - start;//计时
        float ms = duration.count() * 1000.0f;
        std::cout << "总耗时:" << ms << 'ms' << std::endl;//输出计时
    }
};

pair类

一个pair保存两个数据成员,类似容器,pair用来生成一个特定类型的模板,创建一个pair时提供两个类型名

容器map里面存放的就是一个一个的pair

//自动初始化为空
pair<std::string,int> pri;
pair<int,std::vector<int>> piv;
//创建时初始化
pair<std::string,std::string> prr1{"James","Dirk"};
//make_pair初始化
pair<std::string,std::string> prr2 = make_pair("James","Dirk");
//读取
std::cout<<prr.first<<prr.second<<std::endl;

结构化绑定

C++17增加的特性,auto[type1,type2...]可以接受返回值是一个元组或者队组的函数

std::tuple<std::string, int> creatPerson()
{
    return { "张三",24 };
}

int main();
{
    auto [name, age] = creatPerson();//结构化绑定
    std::cout << name << age << std::endl;
    return 0;
}

Effective C++

条款1:视C++为一个语言联邦

一开始C++只是C加上一些面向对象的特性,后来随着C++接受各种不同于C with Classes的各种观念、特性和编程战略,Exception对函数结构化带来不同做法,template将我们带到新的设计思考方式,STL定义了前所未有的伸展性做法。

今天的C++已经是已经是个多重范型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。

最简单的方法就是将C++视为一个有相关语言组成的联邦而非单一语言,C++中主要的次语言有四种:

  • C: C++仍是以C为基础。区块blocks、语句statement、预处理器preprocessor、内置数据类型build-in data types、数组arrays、指针points等等天天来自C。许多时候C++对问题的解法不过是较高级的C解法,当你以C++内C的成分工作时,高效的编程守则映照出C语言的局限

  • Object-Oriented C++: 面向对象的部分。包含了classes(构造和析构)、封装encapsulation、继承inheritance、多态、virtual函数等等。

  • Template C++:这是C++泛型编程的部分,也是大多数程序员经验最少的部分。Template相关考虑与设计以及弥漫整个C++,良好编程守则中“惟template适用”的特殊条款并不罕见。实际上由于templates威力强大,它们带来崭新的编程范型,也就是所谓的TMP模板元编程,TMP相关规则很少与C++主流编程互相影响

  • STL: STL是一个template的程序库,非常特殊的一个。它对容器contains、迭代器iterators、算法algorithms以及函数对象function objects的规约有极佳的紧密配合与协调。STL有自己特殊的工作方式,当你伙同STL一起工作,你必须遵守它的规约。

当你从一个次语言切换到另一个,高效编程守则则会要求你改变策略

条款2:尽量以const,rumen,inline替换#define

这个条款也可以叫:宁可用编译器替换预处理器,define不被视为语言的一部分,所以编译器无法读取到define语句

#define ASPECT_RATIO 1.653

像上面这句,记号名称ASPECT_RATIO从未被编译器看见,所以在编译器开始处理源代码之前它就被预处理器取走了,于是记号名称ASPECT_RATIO有可能没进入几号表symbol table内。于是当你使用这个常量出现了一个编译错误信息时,这个错误信息只会提到1.653而不是ASPECT_RATIO,这会使开发者困惑。如果ASPECT_RATIO被定义在一个不是我们写的头文件中,我们会对1.653以及来自何处毫无概念,从而导致追踪浪费时间

  • 最好的解决办法就是用一个常量替换上述的宏

    const double AspectRatio = 1.653;

    AspectRatio作为一个常量会被编译器看到并记录在记号表内

    以常量替换#defines两种需要注意的情况:

  1. 定义常量指针:由于常量定义式常常被放在头文件内,以便被不同的源码含入,因此有必要将指针声明为const。如果指针指向的也是一个不可被修改的值,就必须要写两次const

    const char* const authorName = "Scott Meyers";

    上面是C风格的字符串char[],对于C++而言使用string更好

    const std::string authorName = "Scott Meyers";
  2. class专属常量:为了将常量的作用域限制于class内,你必须让它成为class的一个成员;而为确保此常量至多只有一份实体,你必须让它成为一个static成员

    class GamePlayer{
    private:
    	static const int NumTurns = 5;//常量声明式
        int scores[NumTurns];//使用该常量
        ...
    }

    上述为NumTurns的声明式而非定义式,而C++要求你对你所使用的任何东西提供一个定义。

    但是如果它是一个class的专属常量又是static且为整数类型(int、char、bool),则只要不对其取地址就可以声明并使用它们,无须提供定义式

    如果要对其取地址,编译器就需要你提供相关的定义式,由于声明时已经获得初值,因此定义时不可以再设初值。如果声明时你的编译器版本不允许设置初值,就在定义时设置初值,但是class内其他的属性如scores 要用到这个初值可能会报错

    const int GamePlayer::NumTurns;//NUmTurns的定义

    我们无法利用#define去定义一个class专属常量,因为#define不重视作用域,一旦被宏定义,在之后编译过程中都有效(除非#undef),意味着#define不仅不能够定义class专属常量也不法提供任何封装性,而常量是可以被封装的

  • 对于上述例子,按照一个属于枚举类型enumerated type的数值可权充int被使用,也可以定义为如下:

    class GamePlayer{
    private:
    	enum{ NumTurns = 5};//成为5的一个记号名称,enum不会导致额外的内存分配。
        int scores[NumTurns];//使用该常量
        ...
    }

    枚举类型(enumeration)是 C++ 中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。

    关键字enum——指明其后的标识符是一个枚举类型的名字。枚举常量表——由枚举常量构成。"枚举常量"或称"枚举成员",是以标识符形式表示的整型量,表示枚举类型的取值。

    枚举常量表列出枚举类型的所有取值,各枚举常量之间以","间隔,且必须各不相同。取值类型与条件表达式相同。

    enum color_set1 {RED, BLUE, WHITE, BLACK}; // 定义枚举类型color_set1
    enum week {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; // 定义枚举类型week

    使用enumback两个理由:

    1. enum back的行为某方面说比较像#define而不是const,取一个const地址合法,而取enum地址和#define地址通常是不合法的,enum可以约束防止别人获得一个pointer和reference指向某个整数常量

    2. enum back 是一个很实用的编程技术,很多人都会用到它,更进一步,enumback技术是模版元编程的基本技术(#48)

  • template inline 函数

    对于宏夹带着宏实参,调用函数f:

    #define CALL_WITH_MAX(a,b) f((a)>(b):(a),(b))//宏中所有实参都需要加上()
    int a=5,b=0;
    CALL_WITH_MAX(++a,b);//a自增了两次
    CALL_WITH_MAX(++a,b+10);//a自增了一次

    这种情况下,a的自增次数竟然取决于”和谁比较!“

    这种情况使用template inline 函数就同时获取宏的效率以及可预料行为、类型安全性

    template<typename T>
    inline void CallWithMax(const T &a,const T &b){
        f(a > b ? a , b);
    }
  • 总结:

    有了const、enum和inline,就可以降低对于处理器(尤其是#define)的需求,但并非完全消除#define、#ifdef、#ifndef仍然扮演控制编译的重要角色

    对于单纯常量,最好以const对象和enum替换#define

    对于形似函数的宏macros,最好改用inline函数替换#define

条款3:尽可能使用const

const允许你指定一个语义约束(指定一个不该被改动的对象),而编译器会强制实施这项约束,它允许你告诉编译器和其他程序员某值应该保持不变,只要这(某值保持不变)是事实,你就应该通过const说出来让编译器知道并襄助。

  • class外部修饰global或namspace作用域中的常量

  • 文件、函数、区块作用域中被声明为static的对象

  • class内部static和non-static成员变量

  • 指针所指物、指针本身(在 * 右边修饰指针本身,在 * 左边修饰指针所指物)

     const Widget* pw;相当于 Widget const*pw;//修饰所指物
     Widget* const pw; //修饰指针
  • 对于迭代器而言

    const std::vector<int>::iterator iter =...;//const修饰迭代器本身,类似 T* const
    ​
    std::vector<int>::const_iterator iter =...;//修饰所指物,类似 const T*
  • 在对函数声明时应用

    对函数返回值进行const修饰,可以避免人们对这个返回值进行不合法的操作,所以如果一个值不需要被修改就对它用const修饰,可以省去很麻烦错误

  • const成员函数

    const实施于成员函数的目的:

    使class接口容易被理解,容易得知哪个函数可以改动对象而哪个不行

    使该成员函数可以作用于const对象,使const对象可以被操纵

  • bitwise const:const成员函数表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。但是bitwise constness机制产生的漏洞可以使我们改变成员通过获得静态成员对象的指针,并修改指针内容做到。

  • logical constness:也就是说非常量成员变量加上mutable关键字就能使其在const成员函数内被修改

总结:

  1. 将某些东西声明为const可帮助编译器侦测出错误的用法,const可以作用于对象、函数参数、函数返回类型、成员函数本体

  2. 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”

  3. 当const与non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

条款4:确定对象被使用前已经被初始化

读取未初始化的值会导致不明确的行为以及许多令人不愉快的调试过程

  • 由于在C++中使用不同部分的C++对应的情况也不同,使用array(来自C part of C++)不保证其内容被初始化,而vector(来自STL part of C++)则保证其内容的初始化

    所以最佳处理办法就是“永远在使用对象之前将它初始化”

  • 对于内置类型初始化:手动初始化,在定义的内置类型的时候给与一个初始化值

  • 对于内置类型之外的其他东西,初始化责任落在构造函数上,确保每一个构造函数都将对象的每一个成员初始化

  • 不要混肴赋值和初始化

    class PhoneNumber{...};
    class ABEntry{
    public:
        ABEnrty(const string& name,const string& address,const list<PhoneNumber>& phones);
    private:
        string theName;
        string theAddress;
        list<PhoneNumber> thePhones;
        int numTimesConsulted;
    }
    ​
    ABEntry::ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones){
        theName = name;  //并非初始化,这都属于赋值
        theAddress = address;
        thePhones = phones;
        numTimesConsulted = 0; //内置数据类型不保证在进入构造函数之前初始化
    }

    C++规定,非内置对象成员变量的初始化动作发生在进入构造函数本体之前,这些非内置对象的初始化发生在这些成员的default构造函数被自动调用时

    所以这里ABEntry构造函数最好是使用member initialization list替换复制动作

    ABEntry::ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones)
        :theName(name),  //并非初始化,这都属于赋值
        theAddress(address),
        thePhones(phones),
        numTimesConsulted(0) //内置数据类型不保证在进入构造函数之前初始化
    {}//构造函数本体不需要任何动作

    上述两个构造函数效果相同,但是下面那种效率通常更高一些,因为上面那种先调用了default后用 copyassignment,而第二中只是单单调用了一次(default)copy构造,这样不会浪费成员之间的default构造函数

  • C++中的static对象是指存储区不属于stack和heap、"寿命"从被构造出来直至程序结束为止的对象。这些对象包括全局对象,定义于namespace作用域的对象,在class、function以及file作用域中被声明为static的对象。其中,函数内的static对象称为local static 对象,而其它static对象称为non-local static对象

    C++规定,non-local static 对象的初始化发生在main函数执行之前,也即main函数之前的单线程启动阶段,所以不存在线程安全问题。但C++没有规定多个non-local static 对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是随机的

    local static 对象(函数内,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句

    C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成

总结:

  1. 为内置对象进行手工初始化,因为C++不保证初始化它们

  2. 构造函数最好使用成员初始值,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序和它们在class中声明的次序相同

  3. 为免除“跨编译单元之初始化次序”问题,请以local static 对象替换non-local static对象

条款5:了解C++默默编写并调用哪些函数

面对一个空类empty class,编译器会为它声明一个copy构造函数、一个copy assignment操作符(operator =)和一个析构函数。如果我们没有定义任何的构造函数,编译器则还会声明一个default构造函数

  • 编译器产出的析构函数是non-virtual,除非这个class的base class 自身声明有virtual析构函数

  • copy构造函数和 copy assignment操作符,编译器创建的版本只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象

  • 编译器产出copy assignment操作符必须满足代码合法且有适当机会证明它有意义

    C++编译器拒绝产出copy assignment操作符的情况:

    1. 成员变量包含&const:C++不允许reference改指向不同对象,如果要在一个内含reference成员或const成员的class内部支持赋值操作,你必须自己定义copy assignment操作符

      将reference和const去掉,编译器就可以提供copy assignment操作符:

    2. 如果某个base class将copy assignment 操作符声明为private ,编译器拒绝为其derived class生成一个copy assignment操作符

总结:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符(operator =)以及一个析构函数,但是有些情况并不是这样

条款6:如果不想使用编译器自动生成的函数,就应该明确拒绝

某些情况下,我们希望我们定义的类每个实例化都是独一无二的,这时候我们就不会想使用copy构造函数和copy assignment操作符,因为这两个操作会制造出一模一样的两个对象。通常来说,如果我们不希望class支持某种特定功能,只要我们不声明对应函数就是了,但是这个策略对copy构造函数、copy assignment操作符不起作用,因为上一条条款已经指出,编译器会为我们声明它们。

  • 我们自己将这些难办的函数声明为private:

    所有编译器产出的函数都是public的,为了阻止这些函数被创造出来,我们需要自己声明它们且我们不是必须得将它们声明为public。所以我们可以主动地将不需要的copy构造函数、copy assignment操作符声明为private,这样既可以避免编译器自己生成,也避免了人们调用这些函数

    copy构造函数、copy assignment操作符被声明为private后还需要我们考虑到member函数和friend函数是可以访问到它们的

    我们一般操作就在private中声明copy构造函数、copy assignment操作符而不去定义,甚至连参数名都可以省略,因为它们根本不会被调用

  • 防止对象被拷贝,我们还可以让它继承下类Uncopyable

    class Uncopyable{
    protected:
        Uncopyable(){}
        ~Uncopyable(){}
    private://防止拷贝
        Uncopyable(const Uncopyable&);
        Uncopyable& operator=(const Uncopyable&);        
    };
    ​
    class Person : private Uncopyable{...};//防止Person被拷贝

    对于Person来说,编译器会试着生成copy构造函数、copy assignment操作符,但是在尝试拷贝Person对象时,就会去调用其父类的拷贝函数,由于父类的copy构造函数、copy assignment操作符是private,所以这些调用会被编译器拒绝

总结:为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现,使用像Uncopyable这样的base class也是一种方法

条款7:为多态基类声明virtual析构函数

一个父类指针指向子类对象实现多态的时候(通常子类都存在在heap中),而父类中没有定义虚析构函数的化,通过父类指针来删除子类对象就会出现无法调用子类的析构函数从而导致子类对象并没有被销毁,形成资源泄露、败坏。

  • 给带多态性质的base class一个virtual析构函数:删除这个子类对象就会成功的调用其的析构函数

  • 如果一个class不包含一个virtual析构函数,通常表示它并不意图被用作一个带多态性质的base class,一个不做为带多态性质的base class 的类也不应该定义virtual析构函数

  • 所有的STL容器都是包含一个non-virtual 析构函数,所以把STL容器当成一个带多态性质的父类是会容易出现上述问题的

  • 并非所有的base class被设计目的都是为了多态用途,标准的STL容器都不被设计作为base class使用,给带多态性质的base class一个virtual析构函数这个规则不适用于非多态性质的base class

总结:

  1. polymorphic base classes 应该声明一个virtual析构函数,如果class带有任何virtual函数,他就应该拥有应该virtual析构函数

  2. Classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性,就不应该声明virtual析构函数

条款8:别让异常出现在析构函数

当一个容器被销毁时,它有责任销毁其内部所有的Wedgets(类),如果这个类的内部含有10个Wedgets,当析构第一个元素时,有一个异常抛出,其他9个Widgets还是应该被销毁(否则保存的任何资源会发生泄露),因此应该调用它们各个析构函数,第二个元素析构时又抛出了一个异常,选择会有两个异常,两个异常会导致程序结束或者出现不明确的行为。所以C++不喜欢析构函数抛出异常

  • 当析构时抛出异常情况,通常有两种方法:

    1. 如果抛出异常,通过调用abort来结束程序:强迫结束程序可以阻止异常从析构函数中传播出去,抢先制“不明确的行为”于死地

    2. 吞下抛出的异常,制作运转记录,记下异常:吞掉异常会压制“某些动作失败”的信息,但是负担比草率结束程序和不明确的行为要轻

    但是这两种方法都无法对异常情况做出反应

  • 重新设计类的接口,使其客户有机会对可能出现的问题作出反应:把析构函数的职责从析构函数上移到客户的手上(设计一个接口页可以完成析构函数的功能),给客户一个处理错误的机会(异常出现在非析构函数是并没有像出现在析构函数那么危险),客户可以忽略它而依赖析构函数,最后发生异常吞下或结束程序,客户没有立场抱怨

总结:

  1. 析构函数绝对不要抛出异常,如果出现了异常,析构函数应该捕捉任何异常,然后吞下或结束程序

  2. 客户需要对某个操作函数允许期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作

条款9:绝不在构造和析构过程中调用virtual函数

class Transaction {
public:
    Transaction() {
        logTransaction(); //希望继承类在创建时就会记录下每个继承类对应的记录
    };
    virtual void logTransaction() {
        std::cout << "log" << std::endl;
    }
};
​
class BuyTransaction : public Transaction {
public:
    virtual void logTransaction() {
        std::cout << "buy" << std::endl;
    }
};
​
int main() {
    BuyTransaction b; //但是这里创建一个BuyTransaction类会输出log
    return 0;
}

上述代码中在BuyTransaction类创建之前需要创建Transaction类,而Transaction类创建时构造函数中包含了一个实现记录的虚函数,输入执行BuyTransaction b的时候会有两者结果:

  1. 如果这个虚函数在父类中有实现的话,就会在这次父类创建执行这个虚函数在父类中的定义的操作,所以最后导致创建一个子类的时候却执行的是父类中的虚函数的定义,导致了多态失效

  2. 如果这个函数是纯虚函数的话,编译器就会在父类构造函数调用这个函数时认为这个纯虚函数没有定义,会发出警告

解决方案1:对于上述的代码的问题,我们将在构造函数中调用的虚函数放在另一个接口中,另外定义一个init()接口,每个子类实例化的之后需要调用这个init()接口,就可以很好的实现多态。看下面代码

#include<iostream>
​
class Transaction {
public:
    Transaction() {};
    void init() {
        logTransaction();
    }
    virtual void logTransaction() {
        std::cout << "log" << std::endl;
    }
};
​
class BuyTransaction : public Transaction {
public:
    virtual void logTransaction() {
        std::cout << "buy" << std::endl;
    }
};
​
int main() {
    BuyTransaction b;
    b.init(); //输出的是buy
    return 0;
}

解决方案2:Transaction中将logTransaction函数改为非虚类,要求BuyTransaction类构造函数传递必要的信息给到Transaction构造函数,而后那个构造函数就可以安全的调用这个非虚类logTransaction函数

无法使用virtual函数从父类向下调用,但是可以在构造期间,令子类将必要的特别的构造信息向上传递到父类构造函数加以弥补(依靠的就是条款4中的构造函数时的成员初始化列表

总结:在构造和析构过程中不要调用virtual函数,因为这类调用从不下降到子类中

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值