🍉C++学习笔记(一) 🍊C++学习笔记(二) 🍅C++学习笔记(三)

第四章

运算符重载的概念

概念

运算符重载:给已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时产生不同行为。

目的

使得C++中的运算符也能够来操作对象

绝大多数运算符可以重载,除下表的7种:

名称 符号
成员访问运算符 .
成员指针访问运算符 . , ->
域运算符 ::
长度运算符 sizeof
条件运算符 ?:
预处理运算符 #

实质

编写以运算符为名称的函数,使用运算符的表达式就被解释为对重载函数的调用

规则

1) 重载运算符应符合原有用法习惯。
2) 运算符重载,不能改变运算符原有的语义,包括运算符的优先级和结和性
3) 不能改变运算操作数的个数及语法结构,及超出c++语言允许重载范围。
4) 重载运算符“()” “ [] ” “ -> ”或者赋值运算符“ = ”时,只能将他们重载为成员函数,不能重载为全局函数
5) 运算符重载不能改变运算符用于基本数据类型对象的含义。可以用于自定义类型对象与基本数据类型对象之间的混合运算。

运算符函数的格式

返回值类型 operator运算符(形参表){函数体···}

例:

1
2
myComplex operator-(const myComplex &c );//声明成员函数
friend myComplex operator+(const myComplex &c1,const myComplex &c2 );//声明友元函数

注意

运算符可以被重载为全局函数(通常为类的友元函数,因为全局函数不能访问类的私有成员),对于二元运算符需要传递两个参数。
运算符可以被重载为成员函数,对于二元运算符,只需要传递一个参数。

重载运算符示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<iostream> 
using namespace std;
class myComplex{//复数类
private:
double real,imag;//复数的实部,虚部
public:
myComplex();//声明构造函数
myComplex(double r,double i);
void outCom() ;//成员函数
myComplex operator-(const myComplex &c );//声明成员函数
friend myComplex operator+(const myComplex &c1,const myComplex &c2 );//声明友元函数


};
myComplex::myComplex(){//定义构造函数
real = 0; imag = 0;
}
myComplex::myComplex(double r,double i){//定以构造函数
real = r,imag = i;
}
void myComplex::outCom(){
cout<<"("<<real<<","<<imag<<")";
}
myComplex myComplex::operator-(const myComplex &c ){
return myComplex(this->real-c.real,this->imag-c.imag);
}//重载成员函数,一个参数返回一个临时对象,this可以省略
myComplex operator+(const myComplex &c1,const myComplex &c2 ){
return myComplex(c1.real+c2.real,c1.imag+c2.imag);
}//重载友元函数,两个参数返回一个临时对象


int main(){
myComplex c1(1,2),c2(3,4),res;

c1.outCom() ;
cout<<"operator+";
c2.outCom() ;
cout<<" = ";
res = c1+c2;
res.outCom();
cout<<endl;


c1.outCom() ;
cout<<"operator-";
c2.outCom() ;
cout<<" = ";
res = c1-c2;
res.outCom();
cout<<endl;
return 0;
}

(1,2)operator+(3,4) = (4,6)
(1,2)operator-(3,4) = (-2,-2)

重载赋值运算符

说明

用于类运算的运算符通常都要重载,但有两个运算符系统提供了默认重载版本
1)赋值运算符 = :系统默认重载为对象成员变量复制
2)地址运算符 & :系统默认重载为返回任何类对象地址

  例:c1与c2都是复数类的对象
     c1 = c2 //合法,系统默认的重载赋值运算符
     c1 = 7 //错误,数据类型不同需要编写相应重载赋值运算符的函数

注意

1)赋值运算符必须重载为成员函数,不能重载为友元函数
2)为了保持与通常意义下的复制运算符的功能一致,应该让重载赋值运算符仍然能连续使用,及res =c1 =c2;应成立,所以operator = 函数通常要返回引用,返回类型是myComplex &如下面示例:

复数类重载赋值运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include<iostream> 
#include<string.h>
using namespace std;
class myComplex{//复数类
private:
double real,imag;//复数的实部,虚部
public:
myComplex();//声明构造函数
myComplex(double r,double i);
~myComplex() {};//析构函数
myComplex addCom(myComplex c1);//成员函数,调用对象与参与对象c1 相加
void outCom(); //成员函数
void outCom(string s);//成员函数
void changeReak(double r);//声明成员函数

friend myComplex operator+(const myComplex &c1,const myComplex &c2);// 声明友元函数 ,实现c1+c2
friend myComplex operator+(const myComplex &c1,double r );//c1+r
friend myComplex operator+(double r ,const myComplex &c1);//r+c1

friend myComplex operator-(const myComplex &c1,const myComplex &c2);// 声明友元函数 ,实现c1+c2
friend myComplex operator-(const myComplex &c1,double r );//c1+r
friend myComplex operator-(double r ,const myComplex &c1);//r+c1

myComplex &operator = (const myComplex &c);//声明成员函数
myComplex &operator = (double);//声明成员函数

};
myComplex::myComplex(){//定义构造函数
real = 0; imag = 0;
}
myComplex::myComplex(double r,double i){//定以构造函数
real = r,imag = i;
}
myComplex myComplex::addCom(myComplex c1){//定以成员函数,调用对象,一个参数
return myComplex(this->real+c1.real,this->imag+c1.imag);
}
void myComplex::outCom(){ //定义成员函数,输出复数
cout<<"("<<real<<","<<imag<<")"<<endl;
}

void myComplex::outCom(string s){ //定义成员函数
cout<<s<<"=("<<real<<","<<imag<<")"<<endl;
}

void myComplex::changeReak(double r){ //定义成员函数
this->real = r;

}

myComplex operator+(const myComplex &c1,const myComplex &c2 ){
return myComplex(c1.real+c2.real,c1.imag+c2.imag); //c1+c2
}
myComplex operator+(const myComplex &c1,double r ){
return myComplex(c1.real+r,c1.imag); //c1+r
}
myComplex operator+(double r ,const myComplex &c1 ){
return myComplex(r+c1.real,c1.imag); //r+c2
}


myComplex operator-(const myComplex &c1,const myComplex &c2 ){
return myComplex(c1.real-c2.real,c1.imag-c2.imag); //c1-c2
}
myComplex operator-(const myComplex &c1,double r ){
return myComplex(c1.real-r,c1.imag); //c1-c2
}
myComplex operator-(double r ,const myComplex &c1 ){
return myComplex(r-c1.real,c1.imag); //c1-c2
}//重载友元函数,两个参数返回一个临时对象

myComplex &myComplex::operator = (const myComplex & c1){
this->real=c1.real,this->imag=c1.imag;
return *this;
}//赋值运算符重载成员函数

myComplex &myComplex::operator = (double r){
this->real=r,this->imag= 0;
return *this;
}//赋值运算符重载成员函数
int main(){
myComplex c1(1,2),c2(3,4),res;

c1.outCom("\t\t\tc1") ;

c2.outCom("\t\t\tc2") ;

res = c1+c2; //调用友元函数,计算 c1+c2
res.outCom("执行 res = c1+c2->\t\t res");

res = c1.addCom(c2); //调用友元函数,计算 c1+c2
res.outCom("执行 res = c1.addCom(c2)->\t res");

res = c1+5;
res.outCom("执行 res = c1+5;->\t\t res");

res = 5+c1; //调用成员函数 operator = (const myComplex & c1)
res.outCom("执行 res = 5+c1->\t\t\t res");

res = 7; //调用成员函数 operator = (double r),没有该函数,出错
res.outCom("执行 res = 7->\t\t\t res");

res = 7+8;//调用成员函数 operator = (double)
res.outCom("执行 res = 7+8->\t\t res");

res =c1 =c2;//两次调用成员函数 operator = (const myComplex & c1)
c1.outCom("\t\t\t\tc1") ;
c2.outCom("\t\t\t\tc2") ;
res.outCom("执行 res =c1 =c2->\t\tres");
return 0;
}





                             c1=(1,2)                            c2=(3,4)

执行 res = c1+c2-> res=(4,6)
执行 res = c1.addCom(c2)-> res=(4,6)
执行 res = c1+5;-> res=(6,2)
执行 res = 5+c1-> res=(6,2)
执行 res = 7-> res=(7,0)
执行 res = 7+8-> res=(15,0)
c1=(3,4)
c2=(3,4)
执行 res =c1 =c2-> res=(3,4)

浅拷贝

同类对象之间可以通过复制运算符“ = ”互相赋值。如果没有重载。“ = ”的作用就是将赋值号右侧的对象一一赋值给左侧的对象。这相当于与值的拷贝,称为值拷贝。
如果赋值的对象中涉及指针引用则他们之间相互关联,对象中的指针指向同一个内存地址。

浅拷贝含义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;
class pointer{
public://公有,main函数可以处理成员
int a;
int *p;//指向整形数的指针
pointer(){
a = 100; // 构造函数
p = new int(10); //创建指针地址空间 并赋初值10
}
pointer(const pointer &tempp) { //复制构造哈数
if(this != &tempp){// 避免a =a这样的赋值
a = tempp.a;
p = tempp.p;
}
}
};
int main(){
pointer p1;//使用构造函数
pointer p2(p1);//使用复制构造函数
pointer p3 = p1; //使用复制构造函数
//*p1.p = 20;
p1.a = 300;
cout<<"\n初始化后,各对象的值及内存地址"<<endl;
cout<<"对象名\t对象地址 a的地址 a的值 p中的值 p指向的值 p的地址" <<endl;
cout<<"p1:\t"<<&p1<<", "<<&p1.a<<" "<<p1.a<<", "<<p1.p<<", "<<*p1.p<<", "<<&p1.p<<endl;
cout<<"p2:\t"<<&p2<<", "<<&p2.a<<" "<<p2.a<<", "<<p2.p<<", "<<*p2.p<<", "<<&p2.p<<endl;
cout<<"p3:\t"<<&p3<<", "<<&p3.a<<" "<<p3.a<<", "<<p3.p<<", "<<*p3.p<<", "<<&p3.p<<endl;
}

1
2
3
4
5
6
7
 
初始化后,各对象的值及内存地址
对象名 对象地址 a的地址 a的值 p中的值 p指向的值 p的地址
p1: 0x6ffdf0, 0x6ffdf0 300, 0x1b1530, 10, 0x6ffdf8
p2: 0x6ffde0, 0x6ffde0 100, 0x1b1530, 10, 0x6ffde8
p3: 0x6ffdd0, 0x6ffdd0 100, 0x1b1530, 10, 0x6ffdd8

浅拷贝可能出现的问题

1)重复释放同一块空间产生错误
改变p1对象的值,就会出现浅拷贝问题

如:当对象p1消亡时,需要释放构造函数中new()动态申请空间。而当对象p2消亡时也会释放这个空间,造成重复释放同一块空间,程序出错。

     *p1.p = 20;
      p1.a = 300;

1
2
3
4
5
6
7
8

初始化后,各对象的值及内存地址
对象名 对象地址 a的地址 a的值 p中的值 p指向的值 p的地址
p1: 0x6ffdf0, 0x6ffdf0 300, 0x181530, 20, 0x6ffdf8
p2: 0x6ffde0, 0x6ffde0 100, 0x181530, 20, 0x6ffde8
p3: 0x6ffdd0, 0x6ffdd0 100, 0x181530, 20, 0x6ffdd8



2)某块内存永远不会被释放而成为内存垃圾
     pointer p4;
     p4 = p1;
创建对p4时,为p4中的成员变量p分配了空间,并符初始值10,执行语句“p4 = p1;”p4中的成员变量p指向了p1中p指针指向的地址,而丢弃了原来指向的地址,这块内存成为内存垃圾。

深拷贝

重载赋值运算符后,赋值语句的功能是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方,这样的拷贝叫“深拷贝”

深拷贝的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;
class pointer{
public://公有,main函数可以处理成员
int a;
int *p;//指向整形数的指针
pointer(){
a = 100; // 构造函数
p = new int(10); //创建指针地址空间 并赋初值10
}
pointer(const pointer &tempp) { //复制构造函数
if(this != &tempp){// 避免a =a这样的赋值
a = tempp.a;
p = new int();
*p = *tempp.p;
}
}
~pointer(){
if(p != NULL){
delete p;
}
}
pointer &operator =(const pointer &c){
if(this == &c){
return *this;
}
delete this->p;
this->p = new int(*c.p);
return *this;
}
};
int main(){
pointer p1;//使用构造函数
pointer p2(p1);//使用复制构造函数
pointer p3 ;
p1 = p1;//赋值重载函数
p3 = p1;// 赋值重载函数 &operator =(const pointer &c)
cout<<"\n初始化后,各对象的值及内存地址"<<endl;
cout<<"对象名\t对象地址 a的地址 a的值 p中的值 p指向的值 p的地址" <<endl;
cout<<"p1:\t"<<&p1<<", "<<&p1.a<<" "<<p1.a<<", "<<p1.p<<", "<<*p1.p<<", "<<&p1.p<<endl;
cout<<"p2:\t"<<&p2<<", "<<&p2.a<<" "<<p2.a<<", "<<p2.p<<", "<<*p2.p<<", "<<&p2.p<<endl;
cout<<"p3:\t"<<&p3<<", "<<&p3.a<<" "<<p3.a<<", "<<p3.p<<", "<<*p3.p<<", "<<&p3.p<<endl;

}
1
2
3
4
5
6
7
 
初始化后,各对象的值及内存地址
对象名 对象地址 a的地址 a的值 p中的值 p指向的值 p的地址
p1: 0x6ffdf0, 0x6ffdf0 100, 0xae1530, 10, 0x6ffdf8
p2: 0x6ffde0, 0x6ffde0 100, 0xae1550, 10, 0x6ffde8
p3: 0x6ffdd0, 0x6ffdd0 100, 0xae1c20, 10, 0x6ffdd8

重载流插入运算符和提取流运算符

1、C++中,可以通过重载流插入运算符 “<<” 和提取流运算符 “>>” 来实现自定义数据类型的输入输出操作。下面是相关的要点:

重载流插入运算符 “<<” 用于输出数据到流中,其通常的形式为:

1
2
3
4
ostream& operator<< (ostream& out, const MyType& data) {
// 输出操作
return out;
}

2、其中,out 表示输出流对象,MyType 表示自定义的数据类型,data 表示要输出的数据对象。函数内部的输出操作可以使用流输出运算符 << 或其他输出函数实现。函数返回 out 对象的引用,以支持链式调用。
重载提取流运算符 “>>” 用于从流中读取数据,其通常的形式为:
提取运算符函数需要返回新的对象值,所以只能使用引用,
1
2
3
4
5

istream& operator>> (istream& in, MyType& data) {
// 输入操作
return in;
}

可以对两个运算符进行重载,使之用于自定义对象。但重载函数不能是流类库中成员,而而必须重载为类的友元
其中,in 表示输入流对象,MyType 表示自定义的数据类型,data 表示要读取的数据对象。函数内部的输入操作可以使用流输入运算符 >> 或其他输入函数实现。函数返回 in 对象的引用,以支持链式调用。
重载流插入运算符和提取流运算符通常需要在类的内部进行定义,以便访问类的私有成员变量和函数。
重载流插入运算符和提取流运算符可以进行重载,以支持不同的数据类型和输入输出格式。常见的输出格式包括十进制、十六进制、科学计数法等,常见的输入格式包括忽略空白字符、检查输入是否合法等。
在进行输入操作时,需要特别注意输入数据的合法性,以避免程序出错或导致安全漏洞。可以使用流状态标志和异常处理机制来检测和处理输入错误。

重载自增、自减运算符

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
using namespace std;

// 自定义计数器类
class Counter {
public:
Counter() : count(0) {}
Counter(int c) : count(c) {}

// 重载前置自增运算符
Counter& operator++ () {
count++;
return *this;
}

// 重载前置自减运算符
Counter& operator-- () {
count--;
return *this;
}

// 重载后置自增运算符
Counter operator++ (int) {
Counter tmp(*this);
operator++();
return tmp;
}

// 重载后置自减运算符
Counter operator-- (int) {
Counter tmp(*this);
operator--();
return tmp;
}

int getCount() const { return count; }

private:
int count;
};

int main() {
Counter c1(0);

cout << "Initial count: " << c1.getCount() << endl;

++c1; // 前置自增
cout << "After ++c1: " << c1.getCount() << endl;

--c1; // 前置自减
cout << "After --c1: " << c1.getCount() << endl;

c1++; // 后置自增
cout << "After c1++: " << c1.getCount() << endl;

c1--; // 后置自减
cout << "After c1--: " << c1.getCount() << endl;

return 0;
}

文件流

文件输入输出流是C++中用于读写文件的一种机制,可以将文件中的数据读入程序,或者将程序中的数据写入文件。以下是文件输入输出流的要点:

创建输出文件对象并与磁盘文件相关联的格式如下;

1
2
3
4
5
6
7
8
9
10
#include <fstream>

int main() {
std::ofstream outFile("filename.txt"); // 创建一个输出文件对象并打开文件
if (outFile.is_open()) { // 确认文件已成功打开
outFile << "Hello, world!"; // 向文件写入数据
outFile.close(); // 关闭文件
}
return 0;
}

在这个例子中,使用std::ofstream类创建了一个输出文件对象outFile,并将其与名为filename.txt的磁盘文件相关联。如果文件不存在,则会自动创建该文件。然后,可以使用outFile对象向文件写入数据。在完成数据写入后,必须调用close()方法来关闭文件,以确保数据已经被写入磁盘文件

文件输入输出流的头文件为< fstream >。

文件输入输出流分为 ifstream 和 ofstream 两种类型。

ifstream 类型用于从文件中读取数据,而 ofstream 类型用于向文件中写入数据。

  1. 使用文件输入输出流时需要先打开文件,可以使用成员函数 open() 打开文件,使用成员函数 close() 关闭文件。
  2. 在使用文件输入输出流读写数据时,需要使用输入输出运算符(<< 和 >>)或 getline() 函数,以及文件输入输出流对象与数据之间的流操作符(如 ifstream 和数据之间的 <<,或 ofstream 和数据之间的 >>)。
  3. 如果想要从文件的指定位置开始读取数据,可以使用成员函数 seekg();如果想要从文件的末尾开始写入数据,可以使用成员函数 seekp()。
  4. 在进行文件读写操作时,应该注意错误处理,例如检查文件是否成功打开、是否成功读写数据等。
  5. 可以使用文件指针来指定读写位置,例如使用 tellg() 和 tellp() 函数获取文件指针的当前位置,使用 seekg() 和 seekp() 函数设置文件指针的位置。
    注意:不能在文件开始追加数据
    总之,文件输入输出流是C++中处理文件的一种机制,使用它可以方便地读写文件中的数据。

    类的继承与派生

    注意: 一个类不能被多次说明为某个派生类的直接类,但是可以不止一次的成为间接基类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    // 继承方式
    // 公有继承
    class DerivedClass : public BaseClass {
    // ...
    };
    // 私有继承
    class DerivedClass : private BaseClass {
    ……
    // ...
    };
    // 保护继承
    class DerivedClass : protected BaseClass {
    ...

    };

    // 访问控制
    class DerivedClass : public BaseClass {
    public:
    void someFunction() {
    publicMemberVariable = 42;
    protectedMemberVariable = 42;
    // 私有成员无法直接访问
    // privateMemberVariable = 42;
    }
    private:
    // ...
    };

    // 构造函数和析构函数
    class DerivedClass : public BaseClass {
    public:
    DerivedClass(int someArg) : BaseClass(someArg) {
    // ...
    }
    ~DerivedClass() {
    // ...
    }
    };

    // 虚函数
    class BaseClass {
    public:
    virtual void someFunction() {
    // ...
    }
    // ...
    };
    class DerivedClass : public BaseClass {
    public:
    virtual void someFunction() override {
    // ...
    }
    // ...
    };

    // 多态性
    void someFunction(BaseClass* someObject) {
    someObject->someFunction();
    }

    BaseClass* obj1 = new BaseClass();
    BaseClass* obj2 = new DerivedClass();
    someFunction(obj1); // 调用BaseClass::someFunction()
    someFunction(obj2); // 调用DerivedClass::someFunction()

    多重继承

    1、 二义性问题
    c++中的二义性问题指的是在派生类中访问基类成员时,由于多重继承或虚继承导致成员名字冲突,编译器无法确定调用哪个成员函数或变量,从而出现歧义。这种歧义被称为二义性。

二义性问题主要出现在以下情况:
多重继承:当派生类同时从两个或多个基类中继承同名成员时,派生类中访问该成员就会出现二义性。在这种情况下,可以使用作用域解析运算符“::”来指定具体调用哪个基类的成员。
虚继承:当多个派生类都以虚继承的方式继承自同一个基类,并且基类中有同名的虚函数或成员变量时,派生类中访问该成员就会出现二义性。在这种情况下,需要通过动态绑定来解决二义性问题。

解决二义性问题的方法主要有以下几种:

  1. 使用作用域解析运算符“::”来指定具体调用哪个基类的成员。
  2. 将二义性成员在派生类中重新定义,避免继承自基类。
  3. 在派生类中重新定义基类的虚函数,并在其中调用基类的同名虚函数。
  4. 使用虚继承或将多继承转化为单继承来避免二义性。

对象之间的信息传递

在面向对象编程中,对象之间的信息传递可以通过不同的方式实现,主要包括以下几种:
1、对象之间的方法调用:一个对象可以调用另一个对象的方法,以获取或改变另一个对象的状态。这种方式是最常见的对象之间信息传递方式,它通过对象的接口提供对其他对象的访问权限,实现对象之间的相互作用。
2、对象之间的消息传递:这种方式类似于进程之间的消息传递。一个对象可以向另一个对象发送消息,以请求另一个对象执行某些操作。这种方式需要对象之间具有某种通信机制,比如消息队列、信号量等。
3、对象之间的事件通知:这种方式常用于 GUI 编程中,当某个对象发生变化时,会通知其他对象来做出相应的响应。例如,在窗口中点击按钮时,按钮对象会发出事件通知,窗口对象可以接收到事件通知并执行相应的操作。
4、对象之间的回调函数:这种方式常用于异步编程中,一个对象可以向另一个对象注册一个回调函数,当某些事件发生时,会调用回调函数。例如,在网络编程中,一个对象可以向另一个对象注册一个回调函数,当网络数据到达时,会调用回调函数来处理数据。

总之,对象之间的信息传递是面向对象编程中的一个核心概念,它可以通过不同的方式实现,以满足不同的需求。

虚函数

C++中的虚函数是一种特殊的成员函数,它可以在基类中被声明为虚函数,这样在派生类中重写(覆盖)它时,就可以实现运行时多态性也称为动态绑定或后期绑定)。

声明为虚函数的语法格式为在函数声明前面加上关键字*virtual*,如下所示

1
2
3
4
5
6
7
class Base {
public:
virtual void print() {
std::cout << "This is the Base class print function." << std::endl;
}
};


这里定义了一个名为print的虚函数。当这个函数在基类中声明为虚函数后,派生类中如果有同名函数的话,会覆盖掉基类中的函数,而不是像非虚函数那样隐藏掉基类的同名函数。当通过基类的指针或引用调用虚函数时,*程序会根据指针或引用实际指向的对象类型来确定该调用哪个类的虚函数,这就实现了多态性*

需要注意的是,虚函数必须以指针或引用的方式进行调用,如果直接调用,则会调用基类的函数而不是派生类的函数。此外,虚函数的调用开销较大,因为需要在运行时进行动态绑定,所以在设计程序时应该慎重使用虚函数。

虚基类

C++虚基类机制是为了解决多重继承带来的问题而提出的一种机制。在多重继承中,如果派生类同时从多个基类中继承了同名的成员变量或函数,就会导致二义性问题。虚基类机制可以让某些基类成为虚基类,使得从这些基类派生出的所有类共享一个基类子对象,从而避免了二义性问题的发生。

在类定义中,将某个基类声明为虚基类时,需要在该基类名称前加上关键字"virtual",例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
int a;
};

class B : virtual public A {
public:
int b;
};

class C : virtual public A {
public:
int c;
};

class D : public B, public C {
public:
int d;
};

在上述代码中,基类A被声明为虚基类,派生类B和C都使用了虚继承方式来继承A。这样,D类就可以通过B和C类共享同一个A类的实例,避免了因为A被多次继承而带来的二义性问题

需要注意的是,在虚继承中,由于存在虚基类指针和虚基类表的影响,会使得派生类对象的内存布局与普通的单一继承或非虚继承有所不同。因此,在使用虚继承时需要格外注意内存管理和初始化等问题

类模板与向量

类模板是C++中的一种模板,用于定义通用类,其中某些成员变量或成员函数的类型不是具体类型,而是模板参数,从而使得可以通过模板参数来实现不同类型的数据处理。类模板可以通过实例化得到具体的类,可以将不同类型的对象进行封装。

向量是C++ STL(标准模板库)中的容器,它可以存储任何类型的对象,使用动态数组实现。向量可以根据需要自动扩展和收缩,提供快速的随机访问和在末尾添加元素的操作,并提供一些其他的常用操作,如查找、排序和删除。向量类似于数组,但具有更高的灵活性和安全性,因为它们可以自动管理内存分配和释放,而不需要手动管理。

类模板和向量的结合可以使程序员轻松地编写支持不同类型的向量类。使用类模板可以将向量类的元素类型作为参数进行泛化,从而使其适用于任何类型的数据,同时向量提供了动态数组的实现,可以实现对变长数据的高效处理,这为数据处理提供了便利。因此,类模板和向量是C++中非常有用的工具,可以提高代码的复用性和可扩展性。

C++中类模板的基本格式如下:

1
2
3
4
5
template <class T>
class ClassName {
// 类的定义
};


其中,template关键字表示声明一个模板,<class T>是模板参数列表,T是模板参数名,可以根据需要自行更改。在类模板中,可以使用模板参数T来定义类的成员变量、成员函数、嵌套类型等。例如:

1
2
3
4
5
6
7
8
9
10
11
template <class T>
class Stack {
private:
T* data;
int size;
public:
Stack();
void push(T x);
T pop();
bool empty();
};

上述代码定义了一个类模板Stack,使用了模板参数T来定义数据类型,包含了一个T*类型的指针成员变量data,表示一个动态数组,以及整型成员变量size,表示数组大小。成员函数包括一个默认构造函数、一个push()函数、一个pop()函数和一个empty()函数,都使用了模板参数T来定义函数参数和返回值类型。

使用类模板创建对象的格式如下:

1
2
3
4
5
6
7
8
template <class T>
class ClassName {
// 类定义
};

// 创建对象
ClassName<data_type> object_name;
//类模板名<模板参数> 对象名1,对象名2;

抽象类

在C++中,抽象类是指含有纯虚函数的类。纯虚函数是指在基类中没有实现的虚函数,需要在派生类中进行实现。

1
2
3
4
5
6
7
8
9
10
11
class AbstractClass {
public:
// 纯虚函数
virtual void pureVirtualFunction() = 0;
// 普通成员函数
void normalFunction() {
// 函数实现
}
// 析构函数
virtual ~AbstractClass() {}
};

其中,纯虚函数通过在函数声明后加上 =0 来表示。抽象类可以包含普通成员函数和成员变量,也可以包含构造函数和析构函数,但需要注意的是,抽象类不能被实例化,只能被继承。如果派生类没有实现纯虚函数,那么派生类也会变成抽象类。

纯虚函数

基类中某个虚函数给不出或没必要给出详细定义,可以将它声明为一个纯虚函数

预处理命令

预处理命令是在程序编译之前执行的命令,通常以“#”开头,用于指导编译器进行某些操作,如定义宏、包含头文件、条件编译等。预处理命令是由预处理器来处理的,预处理器将处理后的代码传递给编译器进行编译。

常用的预处理命令包括:

  1. #define:定义宏,将一个标识符替换为一个常量表达式、变量表达式、函数等。
  2. #include:包含头文件,将指定的文件内容插入到当前文件中。
  3. #ifdef、#ifndef、#endif:条件编译,用于根据条件选择性地编译代码。
  4. #pragma:指示编译器执行某些特定操作,如设置编译器选项、打开或关闭警告信息等。
  5. #undef:取消宏的定义。
    预处理命令的使用可以提高程序的可读性和可维护性,同时也可以减少代码的重复编写。

    结构体

    分析题目:
    1
    struct D { int a;union{int b;double c;} ;D * d[2];} //该类型的大小为多少字节、
    根据结构体的对齐原则,结构体变量的地址应该是该结构体中最大数据成员所占字节数的整数倍,因此需要考虑各个成员的大小以及对齐方式。

对于该结构体,其中最大的数据成员为 union 中的 double 类型,占用 8 个字节。因此,该结构体在内存中的大小应该为 8 + 2 * 8 = 24 个字节。其中,2 表示数组 d 包含两个指针类型的元素,每个指针类型的大小为 8 个字节;union 的大小应该取决于其最大的成员类型的大小,即 double 类型所占用的 8 个字节。

需要注意的是,结构体中的内存对齐方式与编译器有关,不同编译器可能会有不同的对齐方式,因此结构体的大小可能会有所不同。

Regenerate response