cpp复习
本文最后更新于:2024年7月30日 中午
C++知识
C++三大特性
封装
封装:一种将抽象性函数接口的实现细节部分包装、隐藏起来的方法。同时,它也是一种防止外界调用端,去访问对象内部实现细节的手段,这个手段是由编程语言本身来提供的。
- 让代码易于理解、修改;
- 加强代码的安全性。
继承
继承:继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。
多重继承
在现实生活中,一些新事物往往会拥有两个或者两个以上事物的属性,为了解决这个问题,C++引入了多重继承的概念,C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。
1 |
|
对于以下的多重继承:
1 |
|
有两个问题:
- 类DC的对象中,存在两份来自类BC0的成员K,如何区分?
- 类DC的对象中存在多个同名成员 x, 应如何使用?
正确访问方式:
1 |
|
虚继承
我们若只想保留一份上述例子中的K,则需要在BC1类和BC2类继承BC0时,其前面加上virtual关键字就可以实现虚拟继承,使用虚拟继承后,当系统碰到多重继承的时候就会先自动加一个BC0的拷贝,当再次请求一个BC0的拷贝时就会被忽略,以保证继承类成员函数的唯一性。
1 |
|
- 虚继承后,子类会存在一个虚函数指针占4字节。
- 继承相关的内存大小需细化
virtual关键字
在 C++ 中,虚函数(virtual function)是一个可以被子类重写的成员函数,而纯虚函数(pure virtual function)是一个在基类中声明的虚函数,但不会在基类中实现,而是要求派生类中实现的函数。
区别如下:
- 虚函数是有实现的,而纯虚函数没有实现。虚函数在基类中有默认实现,子类可以重写它,也可以不重写,但纯虚函数必须在子类中实现。
- 如果一个类中包含至少一个纯虚函数,那么这个类就是抽象类,不能直接实例化对象。而虚函数不会强制一个类成为抽象类。
- 调用纯虚函数会导致链接错误,除非在派生类中实现该函数。而虚函数可以被调用,如果派生类没有重写该函数,将调用基类的实现。
- 纯虚函数可以为接口提供一个规范,子类必须实现这些接口。而虚函数则允许子类通过重写来扩展或修改父类的实现。
- 纯虚函数只能在抽象类中声明,而虚函数可以在任何类中声明
- 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
- 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
运行时多态
如果父类函数未使用virtual关键字,则调用子类函数时,默认调用父类写好的函数。即静态绑定。在删除时, 只会调用基类的析构函数, 而不会调用子类的析构函数. 如果将指针的类型声明为子类类型, 那么调用顺序是先调用子类的析构函数, 再调用基类的析构函数.
加入了virtual的函数,一个类中函数的调用并不是在编译的时候决定下来的,而是在运行时候被确定的,这也就是虚函数.
虚函数表和虚函数指针
C++ 中虚函数这种多态的性质是通过虚函数表指针和一张虚函数表来实现的:
- vptr(虚函数表指针, 占 4 个字节): 一个指向虚函数表的指针,每个对象 都会拥有这样的一个指针. C++ 的编译器将虚函数表指针存放在对象实例中最前面的位置, 这是为了保证取得虚函数表时具有最高的性能.
- vtable(虚函数表): 每一个含有虚函数的类都会维护一个虚函数表,里面按照声明顺序记录了该类的全部虚函数的地址
在进行虚函数的调用时, 编译器会根据基类指针所指向(或者基类引用所引用)的对象中的虚函数表指针找到该类的虚函数表, 然后在虚函数表中查找要调用的虚函数的地址
通常情况下,编译器在下面两处地方添加额外的代码来维护和使用虚函数表指针:
- 在每个构造函数中。此处添加的代码会设置被创建对象的虚函数表指针指向对应类的虚函数表
- 在每次进行多态函数调用时。 无论何时调用了多态函数,编译器都会首先查找vptr指向的地址(也就是指向对象对应的类的虚函数表),一旦找到后,就会使用该地址内存储的函数(而不是基类的函数)。
为什么虚函数表指针的类型为void *
因为对于虚函数表来说, 一个类中的所有虚函数都会放到这个表中, 但是不同的虚函数对应的函数指针类型各不相同, 所以这个表的类型也就无法确定.
为什么虚函数表前要加const
因为虚函数表是在编译时, 由编译器自动生成的, 并且不会发生改变, 当有多个B类的实例时, 每个实例都会维护一个虚函数表指针, 但是这些指针指向的都是同一个虚函数表, 该表是一个常量表.
类的 size 与虚函数
- 类中的普通函数,都是不占用类空间的。
- 普通函数只是一种表示, 其本身并不会占有任何内存;
- 如果类中没有任何变量或者虚函数时, 类的 size 不会为1, 而是会自动插入一个字节, 并且在类的 size 大于1的时候, 该字节会被覆盖掉;
- 类的指针大小为8字节;
- 继承时,子类的大小为父类之和,加上子类的自有元素。
- 关于对齐:变量存放的起始地址相对于结构的起始地址的偏移量必须为某个数值的倍数. 同时会根据当前结构中的元素的最大字节数将总的 size 补成最大字节数的倍数
对齐:
1 |
|
输出并非17个字节,而是:
1 |
|
20个字节,因为char向更大的int对齐。
static关键字
C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。
面向过程的static
静态全局变量
1 |
|
静态全局变量特点如下:
- 该变量在全局数据区分配内存;
- 未经初始化的静态全局变量会被程序自动初始化为0;
- 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;
定义全局变量就可以实现变量在文件中的共享,但定义静态全局变量还有以下好处:
- 静态全局变量不能被其它文件所用;
- 其它文件中可以定义相同名字的变量,不会发生冲突;
静态局部变量
通常,在函数体内定义了一个变量,每当程序运行到该语句时都会给该局部变量分配栈内存。但随着程序退出函数体,系统就会收回栈内存,局部变量也相应失效。但有时候我们需要在两次调用之间对变量的值进行保存。通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。
静态局部变量正好可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。
静态局部变量特点如下:
- 在全局数据区分配内存;
- 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
- 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
- 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
静态函数
- 仅能被该文件内使用。
面向对象的static关键字
静态数据成员
- 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
- 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。在Example 5中,语句int Myclass::Sum=0;是定义静态数据成员
1 |
|
- 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
- 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
<数据类型><类名>::<静态数据成员名>=<值> - 类的静态数据成员有两种访问形式:
<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名> - 同全局变量相比,使用静态数据成员有两个优势:
\1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
\2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
静态成员函数
与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。下面举个静态成员函数的例子。
1 |
|
- 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
- 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
- 静态成员函数不能访问非静态成员函数和非静态数据成员;
- 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
- 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
<类名>::<静态成员函数名>(<参数表>)
调用类的静态成员函数。
总而言之,面向对象的static关键字,主要是为整个类服务,而不是类抽象出的对象。
inline 关键字
inline关键字是编译关键字,其作用是将函数展开,把函数的代码复制到每一个调用处。这样调用函数的过程就可以直接执行函数代码,而不发生跳转、压栈等一般性函数操作。可以节省时间,也会提高程序的执行速度。
- 关键字inline必须与函数的定义体放在一起,才能使函数成为内联函数;仅仅将inline放在函数声明前面不起作用;
- inline只适合函数体内代码比较简单的函数使用,不能包含复杂的结构控制语句;
- inline仅是一个对编译器的建议;
- 建议inline的函数定义放在头文件里;
- 慎用inline;内联能提高函数的执行效率,内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
struct和union
结构体struct:把不同类型的数据组合成一个整体。struct里每个成员都有自己独立的地址。
关于struct的内存对齐:
- 必须是最大类型成员的整数倍
1 |
|
- 结构体中定义的空数组,如int b[0],是不占空间的;
- 结构体若为空,会有一个字节的占位符。
union:
- 同时只能有一个成员被赋值;
- 长度为最大的成员长度。
- 由于共享数据空间,不同类型即不同解释方式。
arr,&arr[0],&arr区别
- arr,&arr[0]均是指代数组首地址;&arr指数组地址;
- 区别体现在:前两者+1后,即取第二个元素,而第三个+1后,会跳过整个数组。
char a , char a[], char*a,char *[],char * *a 之间的区别
char a[]指一个字符数组,是有初始化内存的;
char*a,是一个指向char的指针,未初始化时,是一个空指针;
关于char *a[],*的优先级较低,因此这是一个char数组,数组中的每一个元素都是指针,这些指针指向char类型;
char **a
两个**代表相同的优先级,因此从右往左看,即
char*(*a)
char *a不就是一个字符串数组,a代表首地址。那么char * (*a)就是和char *a[]一样的数据结构
C++程序布局
内存从上到下分别是:
- 栈stack |高地址|
- 堆heap
- bss段
- data段
- 代码段text |低地址|
栈:保存函数的局部变量,参数以及返回值。在函数调用后,系统会清楚栈上保存的栈帧和局部变量,函数参数等信息。栈是从高到低增长的。
堆:动态内存分配的都放在堆上。堆是从低到高的。
bss段:(Block Started by Symbol)存放程序中未初始化的全局变量的一块内存区域,在程序载入时由内核置为0。
data段:static变量和所有初始化的全局变量都在data段中。
关于bss和data段
1 |
|
- 上方的程序ar会存入bss,其不占内存空间,仅为一个占位符,记录所需的大小;
- 下方ar会存入data段,占用空间。
const int * p 和 int * const p
const int * p 是常量指针,意思是指向的值,不能被改变;
int * const p 是指针常量,意思是指针指向的位置不能变;
注:指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的。
1 |
|
处理输入输出
处理字符串
1 |
|
需要读取,并把每行的字符串排序
答案:
1 |
|
- 使用
getline(cin,s)
读取整行; - 再通过
stringstream
结构,将其中的字符串提取出来,去除空格。
处理整数输入
1 |
|
答案:
1 |
|
- 使用
cin.get() == '\n'
来判断是否读到末尾。
关于cin.get()
cin.get()
从标准输入接收一个字符,可以是空格和换行。cin.get (ch);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 如果程序正在使用 get 函数简单地暂停屏幕直到按回车键,并且不需要存储字符,则该函数也可以这样调用:
```cin.get();```
### 混用```cin>>``` 和 ```cin.get()```容易产生的错误
- 当```cin>>```读取至换行符时,换行符不会被读入,而是被留在了键盘缓冲区中;
- 上述情况下,若妄图使用```cin.get()```读取下一次输入,可想而知,其会先读入之前的换行符;
## 引用与指针
- 引用就是一个常量指针。
- ```cpp
int i=5;
int &ri=i;
ri=8;
///
int i=5;
int* const pi=&i;
*pi=8;上面的两种代码是完全相同的。
高级语言层面引用与指针常量的关系
相同点:指针和引用在内存中都占用4个或者8个字节的存储空间,都必须在定义的时候给初始化。
指针常量本身(以p为例)允许寻址,即&p返回指针常量本身的地址,*p表示被指向的对象
引用变量本身(以r为例)不允许寻址,&r返回的是被引用对象的地址,而不是变量r的地址(r的地址由编译器掌握,程序员无法直接对它进行存取)
引用不能为空,指针可以为空;
指针数组这一块。数组元素允许是指针但不允许是引用,主要是为了避免二义性。假如定义一个“引用的数组”,那么array[0]=8;这条语句该如何理解?是将数组元素array[0]本身的值变成8呢,还是将array[0]所引用的对象的值变成8呢?
在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。而在引用传递过程中, 被调函数的形参虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址(指针放的是实参变量地址的副本)。
“sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
typedef
typedef故名思意就是类型定义的意思,但是它并不是定义一个新的类型而是给已有的类型起一个别名。主要有两个作用,第一个是给一些复杂的变量类型起别名,起到简化的作用。第二个是定义与平台无关的类型,屏蔽不同平台之间的差异。在跨平台或操作系统的时候,只需要改typedef本身就可以
与 #define的区别
关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。#define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。
对指针操作不同。见下面代码
1 |
|
#include<>和#include“ ”的区别
#include<>
一般用来查找标准库文件所在目录,在编译器设置的include路径内搜索;
#include””
#include “” 的查找位置是当前源文件所在目录
要注意的一点就是,如果我们自己写的头文件,而不是标准库函数中的,那么引用这个头文件要使用
#include""
,而不能使用#include<>
,因为我们自己写的头文件并不在编译器设置的路径内,使用#include<>
会提示无法找到。若 #include “” 查找成功,则遮蔽 #include <> 所能找到的同名文件;否则再按照 #include <> 的方式查找文件。
左值和右值
- 可以取地址的是左值,不能取的是右值;
- 右值引用:
int && a = 1;
可以延长右值的寿命,提高程序效率。
深拷贝和浅拷贝
在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。
当数据成员中没有指针时,浅拷贝是可行的;
但当数据成员中有指针时,会出问题。如果没有自定义拷贝构造函数,会调用默认拷贝构造函数,这样就会调用两次析构函数。第一次析构函数delete了内存,第二次的就指针悬挂了。所以,此时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
例子:
浅拷贝的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class A
{
public:
A(int _size) : size(_size)
{
data = new int[size];
} // 假如其中有一段动态分配的内存
A(){};
~A()
{
delete [] data;
} // 析构时释放资源
private:
int* data;
int size;
}
int main()
{
A a(5), b = a; // 注意这一句
}可以发现,当b = a 后,b和a均需要调用析构函数,会导致data的double free;
正确情况是需要深拷贝的:
1 |
|
extern关键字
extern存储类
如果在一个文件中要引用另一个文件中定义的外部变量,则在此文件中应用extern关键字把此变量说明为外部的。例如:
1 |
|
大型程序为了易于维护和理解,通常需要把程序划分为多个文件来保存,每个文件都可以单独编译,最后再把多个文件的编译结果(即目标文件)连接到一起来生成一个可执行程序。这种情况下,如果在一个文件中需要引用另一个文件中的外部变量,就需要利用extern说明。
C++中this指针相关问题
This指针的来源
通过转化成c语言好理解一点。早期还没有针对特定c++的编译器,因此编译c++的时候都先翻译成c语言,再进行编译。
对于class结构来说,c语言中与之对应的就是结构体。
类中的成员变量可以翻译成结构体域中的变量,但是结构体中没有成员函数这个概念,因此成员函数就翻译成为全局函数。
那么如果函数内部想使用成员数据,可以使用该对象指针的方式,指向成员变量。
其作用就是指向非静态成员函数所作用的对象。
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该非静态成员函数的那个对象。- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)。
this指针是什么时候创建的?
this在成员函数的开始执行前构造,在成员的执行结束后清除。
但是如果class里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new的方式创建对象的话,在堆里分配内存,new操作符通过eax(累加寄存器)返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx
this指针存放在何处?堆、栈、全局变量,还是其他?
this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它们并不是和高级语言变量对应的。
如果我们知道一个对象this指针的位置,可以直接使用吗?
this指针只有在成员函数中才有定义。
因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。
在成员函数中调用delete this会出现什么问题?
在类对象的内存空间中,只有数据成员和虚函数表指针,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。
当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
如果在类的析构函数中调用delete this,会发生什么?
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
运算优先级
从最优先到最靠后:
(),[],->,.,::,++,--
等,访问对象的,以及后置的自增自减!, ~ , ++, -- ,-,+,*,&,sizeof()
等,一元取正,一元取负,解引用,取地址,逻辑取反等;*,/,%
乘、除、取余加法减法;
+
左移右移;
>>
比较大小;
>
等于比较;
==
按位与
&
按位异或
^
按位或
|
逻辑与或
&& , ||
赋值操作符
= , +=
逗号
,
继承的访问权限问题
访问权限:public 可以被任意实体访问,protected 只允许子类(无论什么继承方式)及本类的成员函数访问,private 只允许本类的成员函数访问。
1 |
|
- 派生类内不管是 public、protected、private 继承,总是可以访问基类的 public、protected 成员,基类中的 private 成员永远不能再派生类内直接访问,不论通过哪种方式。
- 派生类实例化的对象
Student st
,只能在public
继承时,才能访问/修改基类的public
成员。其余情况下,派生类对象对基类既无法访问也无法修改。
另外,继承方式会改变从基类继承的成员在派生类的访问权限:
1、public 继承不改变基类成员的访问权限;
2、protected 继承将基类中 public 成员变为子类的 protected 成员,其它成员的访问权限不变;
3、private 继承使得基类所有成员在子类中的访问权限变为 private。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!