当前位置: 首页 > news >正文

C++复习

什么是C++?

 1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此,C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。

C++98

C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)

C++11

增加了许多特性,使得C++更像一种新语言,比如∶正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等

命名空间

在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都将作用于全局作用域中,可能会导致很多命名冲突。
 使用命名空间的目的就是对标识符和名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

缺省参数的概念

 缺省参数是指在声明或定义函数时,为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

函数重载的概念

 函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表必须不同。函数重载常用来处理实现功能类似,而数据类型不同的问题。

引用的概念

 引用不是定义一个变量,而是已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

1. 类和对象

列表初始化

构造函数中的语句只能将其称作为赋初值,而不能称作为初始化。因为初始化只能初始化一次,而构造函数体内可以进行多次赋值。

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。

class Date
{
public:// 构造函数Date(int year = 0, int month = 1, int day = 1):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};

一、每个成员变量在初始化列表中只能出现一次
 因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。
二、类中包含以下成员,必须放在初始化列表进行初始化:
1.引用成员变量

 引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。

2.const成员变量
 被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。

3.自定义类型成员(该类没有默认构造函数)
 若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。

三、尽量使用初始化列表初始化
 因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论你是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)。

explicit关键字

 构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换在语法上,代码中Date d1 = 2021等价于以下两句代码:

Date tmp(2021); //先构造
Date d1(tmp); //再拷贝构造

但是,对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数

static成员

 声明为static的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

class Test
{
private:static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

非静态成员函数可以调用静态成员函数。

友元

  • 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
  •  友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。

内部类

特性

 1、内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。
 2、内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
 3、外部类的大小与内部类的大小无关。

内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。 内部类需要通过A::B的方式定义

2. 泛型编程

函数模板

 如果让你编写一个函数,用于两个数的交换。在C语言中,我们会用如下方法:

C++的函数重载使得用于交换不同类型变量的函数可以拥有相同的函数名,并且传参使用引用传参,使得代码看起来不那么晦涩难懂。

但是,这种代码仍然存在它的不足之处:

  •  1、重载的多个函数仅仅只是类型不同,代码的复用率比较低,只要出现新的类型需要交换,就需要新增对应的重载函数。
  •  2、代码的可维护性比较低,其中一个重载函数出现错误可能意味着所有的重载函数都出现了错误。

函数模板:

在编译器编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如,当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于double类型也是如此。

函数模板的实例化

有现成吃现成的,可以隐式或者显式实例化。

类模板

  • 类模板不支持分离编译,即声明在xxx.h文件中,而定义却在xxx.cpp文件中。
  • 类模板名字不是真正的类,而实例化的结果才是真正的类。Vector<int>才是类

3. C/C++内存分布

C语言中动态内存管理方式

malloc、calloc、realloc和free


void *calloc( size_t num, size_t size );void *realloc( void *memblock, size_t size );void free( void *memblock );

new和delete操作内置类型

申请和释放单个元素的空间,使用new和delete操作符;申请和释放连续的空间,使用new[ ]和delete[ ]。 它等于申请空间加调用构造函数。

	//动态申请10个int类型的空间并初始化为0到9int* p7 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //申请 + 赋值delete[] p7; //销毁

operator new和operator delete函数

        new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new和delete在底层是通过调用全局函数operator new和operator delete来申请和释放空间的。
 operator new和operator delete的用法和malloc和free的用法完全一样,其功能都是在堆上申请和释放空间。

int main()
{//new(place_address)type 形式A* p1 = (A*)malloc(sizeof(A));new(p1)A;//new(place_address)type(initializer-list) 形式A* p2 = (A*)malloc(sizeof(A));new(p2)A(2021);//析构函数也可以显示调用p1->~A();p2->~A();return 0;
}

STL:类模板

迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问

String的模拟实现

关键是以下三个成员:char* _str;      size_t _size;     size_t _capacity; 

//模拟实现string类class string{private:char* _str;       //存储字符串size_t _size;     //记录字符串当前的有效长度size_t _capacity; //记录字符串当前的容量static const size_t npos; //静态成员变量(整型最大值)public:typedef char* iterator;typedef const char* const_iterator;//默认成员函数string(const char* str = "");         //构造函数string(const string& s);              //拷贝构造函数string& operator=(const string& s);   //赋值运算符重载函数~string();                            //析构函数//迭代器相关函数iterator begin();iterator end();const_iterator begin()const;const_iterator end()const;//容量和大小相关函数size_t size();size_t capacity();void reserve(size_t n);void resize(size_t n, char ch = '\0');bool empty()const;//修改字符串相关函数void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);string& insert(size_t pos, char ch);string& insert(size_t pos, const char* str);string& erase(size_t pos, size_t len);void clear();void swap(string& s);const char* c_str()const;//访问字符串相关函数char& operator[](size_t i);const char& operator[](size_t i)const;size_t find(char ch, size_t pos = 0)const;size_t find(const char* str, size_t pos = 0)const;size_t rfind(char ch, size_t pos = npos)const;size_t rfind(const char* str, size_t pos = 0)const;//关系运算符重载函数bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator==(const string& s)const;bool operator!=(const string& s)const;};

2.2.6. vs和g++下string结构

下述结构是在32位平台下进行验证,32位平台下指针占4个字节

vs下string的结构:

string总共占28个字节,内部结构稍微复杂一点

先是一个虚基类指针:4个字节.

再是有一个联合体,联合体用来定义string中字符串的存储空间:当字符串长度小于15(数组尾部存储一个\0字符)时,使用内部固定的字符数组来存放当字符串长度大于等于16时,从堆上开辟空间。故其一共16个字节.

大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。

最后还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量。

一共就是4+16+4+4=28.,所以我们vs平台下我们sizeof一个string对象的大小是28

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

1.空间总大小        2.字符串有效长度        3.引用计数         4.指向堆空间的指针,用来存储字符串

string会直接申请15个字节的空间,少于15个字节数据时数据存放在堆区。

vector的介绍和模拟实现

reserve和resize
通过reserse函数改变容器的最大容量,resize函数改变容器中的有效元素个数。

reserve规则:

  •  1、当所给值大于容器当前的capacity时,将capacity扩大到该值。
  •  2、当所给值小于容器当前的capacity时,什么也不做。

resize规则:

  •  1、当所给值大于容器当前的size时,将size扩大到该值,扩大的元素为第二个所给值,若未给出,则默认为0。
  •  2、当所给值小于容器当前的size时,将size缩小到该值。

vector中的find 函数是在算法模块(algorithm)当中实现的,不是vector的成员函数。这与string之中的find函数不同。

扩容规则

现在C++编译器vector都偏向于1.5倍扩容,当size==capacity,触发扩容。1--2--3--5-这样去扩容。扩容会把原有空间释放,开辟新的空间,引发迭代器失效的问题,为了避免频繁扩容,我们可以先reverse()足够大的空间。

迭代器失效问题

迭代器是typedef的指针,是一个固定的地址,当出现增删操作时,迭代器就可能失效了。使用迭代器时,永远记住一句话:每次使用前,对迭代器进行重新赋值。

模拟实现

在vector当中有三个成员变量_start、_finish、_endofstorage。

list的介绍与实现

  1. list是一种可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立结点当中,在结点中通过指针指向其前一个元素和后一个元素。
  3. 与其他容器相比,list通常在任意位置进行插入、删除元素的执行效率更高。它比vector多了头部删除和头部插入的接口。

List的实现

实现list的时候就需要实现一个迭代器类了 ? 

因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针。但是对于list来说,其各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作。

list的实现中包含node和list_iterator和list三个部分。list只需包含一个head头指针即可。list_iterator中则包含解引用,++,--等操作。

stack和queue

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和queue只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque容器。容器适配器直接调用 其余容器中的方法实现。

stack和queue既可以使用顺序表实现,也可以使用链表实现。在这里我们若是定义一个stack,并指定使用vector容器,则定义出来的stack实际上就是对vector容器进行了包装。记住:类模板加指定类型才是具体的类。

priority_queue的介绍

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中的元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。它也是容器适配器。

默认使用vector作为底层容器,内部构造大堆结构。堆排序中建堆可从最后一个非叶子节点自底向上建堆,时间复杂度为O(N),如果采用向下建堆,时间复杂度为O(N*logN)。

priority_queue<int, vector<int>, less<int>> q1;

定义上使用一个类型,两个类模板定义的类。

namespace cl //防止命名冲突
{//比较方式(使内部结构为大堆)默认template<class T>struct less{bool operator()(const T& x, const T& y){return x < y;}};//优先级队列的模拟实现template<class T, class Container = vector<T>, class Compare = less<T>>class priority_queue{public://堆的向上调整void AdjustUp(int child){int parent = (child - 1) / 2; //通过child计算parent的下标while (child > 0)//调整到根结点的位置截止{if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置{//将父结点与孩子结点交换swap(_con[child], _con[parent]);//继续向上进行调整child = parent;parent = (child - 1) / 2;}else//已成堆{break;}}}//插入元素到队尾(并排序)void push(const T& x){_con.push_back(x);AdjustUp(_con.size() - 1); //将最后一个元素进行一次向上调整}//堆的向下调整void AdjustDown(int n, int parent){int child = 2 * parent + 1;while (child < n){if (child + 1 < n&&_comp(_con[child], _con[child + 1])){child++;}if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置{//将父结点与孩子结点交换swap(_con[child], _con[parent]);//继续向下进行调整parent = child;child = 2 * parent + 1;}else//已成堆{break;}}}//弹出队头元素(堆顶元素)void pop(){swap(_con[0], _con[_con.size() - 1]);_con.pop_back();AdjustDown(_con.size(), 0); //将第0个元素进行一次向下调整}//访问队头元素(堆顶元素)T& top(){return _con[0];}const T& top() const{return _con[0];}//获取队列中有效元素个数size_t size() const{return _con.size();}//判断队列是否为空bool empty() const{return _con.empty();}private:Container _con; //底层容器Compare _comp; //比较方式};
}

这里可以看到类模板定义中有三个类模板参数,第二个是选用的容器,第三个是比较方法,模板默认参数分别是vector<T>,less<T>,同时类模板可以使用非类型参数,传入常量。

函数模板和类模板的特化

产生原因是对传入的特定类型继续特殊处理,类模板特化也有对传入的特定类型进行特殊处理。

模板声明和定义分离会报连接错误,.h和.cpp分开写

模板分离编译失败的原因:

C++中各个文件是进行分开编译的。
在函数模板定义的地方(Add.cpp)没有进行实例化,因为不知道它实例化成什么类型。而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化。

模板在编译阶段进行实例化。具体来说,编译器会根据模板的实际使用情况,在编译期间生成对应类型的函数代码(即 模板实例化

C++继承

继承的概念

想想person和teacher,还有student的继承关系。

继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,而继承便是类设计层次的复用。

派生类对象可以赋值给基类的对象指针和引用,但是基类对象不能直接赋值给派生类对象,基类指针可以通过强制类型转换给派生类指针,但此时基类指针是指向派生类对象才是安全的。

继承中的作用域

在继承体系中的基类和派生类都有独立的作用域。若子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。

父类中的fun和子类中的fun不是构成函数重载,因为函数重载要求两个函数在同一作用域,而此时这两个fun函数并不在同一作用域。为了避免类似问题,实际在继承体系当中最好不要定义同名的成员。

派生类的六个默认成员函数

创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数。

若基类当中定义了一个static静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static成员实例。

例如,在基类Person当中定义了静态成员变量_count,尽管Person又继承了派生类Student和Graduate,但在整个继承体系里面只有一个该静态成员。

多继承

多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承。它会产生二义性问题。

为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承。如前面说到的菱形继承关系,在Student和Teacher继承Person是使用虚拟继承,即可解决问题。

D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a。

在继承方式中,基类的内部细节对于派生类可见,继承一定程度破坏了基类的封装,基类的改变对派生类有很大的影响,派生类和基类间的依赖性关系很强,耦合度高。组合的耦合度低,代码维护性好。不过继承也是有用武之地的,有些关系就适合用继承,另外要实现多态也必须要继承。若是类之间的关系既可以用继承,又可以用组合,则优先使用组合。

多态

多态的构成条件

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

父类指针若指向的是父类对象,则调用父类的虚函数,父类指针若指向的是子类对象,则调用子类的虚函数。

虚函数的重写

虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同),此时我们称该派生类的虚函数重写了基类的虚函数。

虚函数重写的两个例外

协变(基类与派生类虚函数的返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。这样可以防止父类指针析构时的内存泄漏。

final:修饰虚函数,表示该虚函数不能再被重写。

override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。

纯虚函数:在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。

接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

派生类的虚表生成步骤如下:

  1. 先将基类中的虚表内容拷贝一份到派生类的虚表。
  2. 如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚表是什么阶段初始化的?虚函数存在哪里?虚表存在哪里?

虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。虚函数表也是存在于 代码区的。

多态的原理

对象Mike中包含一个成员变量_p和一个虚表指针,对象Johnson中包含两个成员变量_p和_s以及一个虚表指针,这两个对象当中的虚表指针分别指向自己的虚表。虚函数在运行时查看找各自的虚表,找到虚函数地址并运行。

那为什么必须使用父类的指针或者引用去调用虚函数呢?为什么使用父类对象去调用虚函数达不到多态的效果呢?

发生切片,同时会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

对象访问普通函数快还是虚函数更快?

对象访问普通函数比访问虚函数更快,若我们访问的是一个普通函数,那直接访问就行了,但当我们访问的是虚函数时,我们需要需要 this 指针定位具体对象的虚表指针,然后在虚表当中找到对应的虚函数,最后才能调用到虚函数。 需要this指针才能找到 虚表指针。

相关文章:

  • 老牌即时通讯应用Skype被关闭,卒年22岁!
  • 2024 ICPC武汉邀请赛暨湖北省赛 题解
  • [特殊字符]【深度解析】Transformer革命:《Attention Is All You Need》完全解读
  • 数据初步了解
  • Excel Vlookup
  • Flutter 布局
  • Java 内存区域与内存溢出异常
  • 数据结构 --- 栈
  • AI 数字短视频数字人源码开发实用技巧分享​
  • 19.第二阶段x64游戏实战-vector容器
  • Navicat Premium 17 备份,还原数据库(PostGreSql)
  • 第四节:进程控制
  • cookie/session的关系
  • Python基础学习-Day17
  • 第九章,链路聚合和VRRP
  • 编码器型与解码器型语言模型的比较
  • Github打不开怎么办?
  • IDEA Mysql连接失败,移除JDBC驱动程序中的协议列表
  • python学习记录
  • Science Advances:南京大学基于硅光芯片实现非阿贝尔辫子操作,突破量子逻辑门技术
  • 杭温高铁、沪苏湖高铁明起推出定期票和计次票,不限车次执行优惠折扣
  • 中国证监会印发《推动公募基金高质量发展行动方案》
  • 住宿行业迎“最火五一”:数千家酒店连续3天满房,民宿预订量创历史新高
  • “子宫内膜异位症”相关论文男性患者样本超六成?福建省人民医院展开调查
  • 无人机穿越大理崇圣寺千年古塔时“炸机”,当地:肇事者已找到,将被追责
  • 外交部:中方和欧洲议会决定同步全面取消对相互交往的限制