0%

c++

C++

封装

封装

将函数定义到结构体内部,就是封装

带有函数的结构体称为类

成员函数

结构体里面的函数,称为成员函数

this关键字

当在一个类中,有一个成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MyStruct
{
int a;
int b;
int c;
int d;
int init(int a, int b, int c, int d)
{
a = a;
b = b;
c = c;
d = d;
}
}

当用a=a时,编译器不能知道a是参数a还是类中的a。

所以用this关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MyStruct
{
int a;
int b;
int c;
int d;
int init(int a, int b, int c, int d)
{
this->a = a;
this->b = b;
this->c = c;
this->d = d;
}
}

因为this关键字指向的是结构体的首地址,所以this->a就指的是这个成员函数所在的类中的一个成员变量,而不是成员函数中的参数

总结:1.this指针时编译器默认传入的,通常会使用ECX寄存器进行传递

2.成员函数都有this指针,无论是否使用,都会有这个指针

3.this指针不能做加减运算,也不能被重新赋值

4.this指针不占用结构体的宽度

构造函数与析构函数

构造函数

构造函数不能有返回值,构造函数的名字跟类名相同

有构造函数时,当创建一个对象时,构造函数会直接被调用

总结:

1.与类名同名并且没有返回值

2.创建对象时执行,主要用于初始化

3.可以有多个相同的构造函数,最好是有一个无参的,称为函数重载,其他的函数也可以重载

4.编译器不需求必须提供,但是提供时必须不能加返回值

析构函数

析构函数也不允许写返回值类型,析构函数只能写一个,不能重载,并且必须无参,不能带参数

构造函数是创建对象的时候执行,而析构函数是当这个对象被销毁时才执行
也就是当函数要return返回值时,析构函数会被执行

总结:

1.只能有一个析构函数,不能重载

2.不能带任何参数

3.不能带返回值

4.主要用于清理工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Person
{
int age;
int level;
char* arr;

Person(int age, int level)
{
this->age = age;
this->level = level;
arr = (char*)malloc(1024);
}
~Person()
{
printf("析构函数执行了...");
free(arr)
}

// 其他函数
}

5.编译器不要求必须提供

继承

有一个Teacher类,一个Person类。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person
{
int age;
int gender;
}

struct Teacher
{
int age;
int gender;
int level;
int teacherID;
}

发现Teacher类中有两个和Person类中是一样的,为了减少代码的重复,可以让Teacher继承Person的一些性质(age、gender)。也就是:

1
2
3
4
5
struct Teacher:Person
{
int level;
int teacherID;
}

这里,Person类称为父类或基类;Teacher类称为子类或者派生类

但是,当父类和子类中有相同的数据时,比如:

1
2
3
4
5
6
7
8
9
10
struct Person
{
int age;
int gender;
}
struct Teacher:Person
{
int age;
int level;
}

当Teacher类继承了Person类时,其中的age重复了,但是编译器仍然会给这两个age都开辟空间

当使用时:

1
2
3
4
5
6
7
int main(void)
{
Teacher t;
t.Person::age = 20; //这里就是父类Person类中的age
t.age = 25; //这里就是子类Teacher类中的age

}

继承不仅限于父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct X
{
int a;
int b;
}
struct Y:X
{
int c;
int d;
}
struct z:Y
{
int e;
int f;
}

当使用时,Z类中能够继承到所有的a,b,c,d

多重继承

与上面类似:

1
2
3
4
5
struct Z:X,Y
{
int e;
int f;
}

多重继承增加了程序的复杂度,不建议使用。

在堆中创建对象

堆中创建对象的两种方式:

1
2
Person* p = (Person*)malloc(sizeof(Person)*10);
Person* p = new Person[10];

释放对象占用内存对应的两种方式:

1
2
free(p);
delete[] p;

当使用malloc函数分配空间时,创建对象后不会调用构造函数,使用free()函数后也不会调用析构函数。

当使用new关键字创建对象时,会调用构造函数,并在使用delete释放对象占用内存时会调用析构函数。

面向对象程序设计之封装和继承

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
#include <stdio.h>
#include <string.h>
#pragma warning(disable:26495)

class Person
{
private:
int Age;
int Gender;
public:
Person()
{

}
Person(int Age, int Gender)
{
this->Age = Age;
this->Gender = Gender;
}
// public method
// Age
void setAge(int Age)
{
if (Age <= 0)
{
this->Age = 0;
}
else
{
this->Age = Age;
}
}
int getAge()
{
return this->Age;
}

//Gender
void setGender(int Gender)
{
if (Gender <= 0)
{
this->Gender = 0;
}
else
{
this->Gender = Gender;
}
}
int getGender()
{
return this->Gender;
}
};

class Teacher:public Person
{
private:
int Level;

public:
Teacher()
{

}
Teacher(int Level, int Age, int Gender):Person(Age, Gender) // 这里如果不在后面加::Person()的话,程序默认会调用Person类中的无参构造函数。
{
this->Level = Level;
}

// public method
// Level
void setLevel(int Level)
{
if (Level <= 0)
{
this->Level = 0;
}
else
{
this->Level = Level;
}
}
int getLevel()
{
return this->Level;
}

};

int main(void)
{
// 创建Teacher对象并传入三个参数
Teacher* A = new Teacher(5, 20, 1);
Teacher* B = new Teacher(4, 22, 0);
printf("A => Age:%d,Level:%d,Gender:%d\n", A->getAge(), A->getLevel(), A->getGender()); // 使用get方法获取
printf("B => Age:%d,Level:%d,Gender:%d\n", B->getAge(), B->getLevel(), B->getGender());

// 使用Teacher中的方法和继承的Person中的方法修改对象A和对象B中的数据
A->setAge(50);
A->setLevel(8);
B->setAge(45);
B->setLevel(9);
putchar('\n');
printf("A => Age:%d,Level:%d,Gender:%d\n", A->getAge(), A->getLevel(), A->getGender());
printf("B => Age:%d,Level:%d,Gender:%d\n", B->getAge(), B->getLevel(), B->getGender());
}

最终输出:

1
2
3
4
5
A => Age:20,Level:5,Gender:1
B => Age:22,Level:4,Gender:0

A => Age:50,Level:8,Gender:1
B => Age:45,Level:9,Gender:0

多态

案例

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
#include <stdio.h>

class Square //这是一个抽象类,抽象类不能创建对象
{
public:
virtual float CalcSq() = 0; //因为不同的图形有不同的求面积方式,所以这里仅定义一个纯虚函数,然后由子类中重写这个函数
};

class Circle : public Square //继承抽象类
{
private:
float r;
public:
Circle(float r)
{
this->r = r;
}
virtual float CalcSq() //重写,这一行必须要和父类中的一样
{
return 3.14 * r * r;
}
};

class Rectangle : public Square
{
private:
float x, y;
public:
Rectangle(float x, float y)
{
this->x = x;
this->y = y;
}
virtual float CalcSq()
{
return x * y;
}
};

float CalcSq(Square* S) //这里传递父类指针
{
return S->CalcSq(); //父类指针可指向子类 ////这里体现了多态性
}

int main()
{
Circle C(2.5); //创建一个对象,参数传递r=2.5
Rectangle R(4.5, 10)//创建一个对象,参数传递x=4.5,y=10

printf("Circle:\t\t%f\n", CalcSq(&C));//调用函数求面积,函数中传递子类的指针,因为float CalcSq(Square* S),其中传递的父类指针可以指向子类地址
printf("Rectangle:\t%f\n", CalcSq(&R));

return 0;
}

虚函数

<1>虚函数目的是提供一个统一的接口,被继承的子类重载,以多态的形式被调用。
<2>如果基类中的函数没有任何实现的意义,那么可以定义成纯虚函数:virtual返回类型函数名(参数列表)=0;
<3>含有纯虚函数的类被称为抽象类(abstract class),不能创建对象。
<4>虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用。

运算符重载

image-20240228230826536

运算符重载也就是给之前的运算符重新起个名字,比如重载一个++,使它被使用时一次+5

当然,这个重载仅在这个类中有用,原本的++还是自增1

模板

使用template <class T>

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
#include <stdio.h>
#include <stdlib.h>

template <class T>
void BubbleSort(T arr[], int length)
{
int i;
int j;
for (i = 0; i < length; i++)
{
for (j = 0; j < length - i - 1; j++)
{
if (arr[j]>arr[j+1])
{
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}


int main(void)
{
int arr1[] = { 5,1,3,2,4,8,9,12,7,8,5,10,6,2,9 };
float arr2[] = { 5.2,1.1,3.6,2.5,4.9,8.0,9.4,1.52,7.7,8.2,5.9,10.4,6.012,9.5 };
BubbleSort<int>(arr1, sizeof(arr1)/sizeof(int));
BubbleSort<float>(arr2, sizeof(arr2)/sizeof(float));

return 0;
}

当程序同为冒泡排序,只是数组的数据类型不同时,可以使用模板替换其中的一部分。

模板可以替换各种类型。如果类型时自己定义的,比如定义一个Number类,要给Number类中的成员进行冒泡排序,则需要先运算符重载,使>可以比较Number中成员的大小。

案例:

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
#include <stdio.h>
#include <stdlib.h>

template <class T>
void BubbleSort(T arr[], int length)
{
int i;
int j;
for (i = 0; i < length; i++)
{
for (j = 0; j < length - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

class Number
{
private:
int x;

public:
Number(int x)
{
this->x = x;
}


bool operator>(Number& num)
{
return this->x > num.x;
}
};

int main(void)
{
int arr1[] = { 5,1,3,2,4,8,9,12,7,8,5,10,6,2,9 };
BubbleSort<int>(arr1, sizeof(arr1) / sizeof(int));

Number arr2[] = { Number(5), Number(1), Number(3), Number(2), Number(4), Number(8), Number(9), Number(12), Number(7), Number(8), Number(5), Number(10), Number(6), Number(2), Number(9) };
BubbleSort<Number>(arr2, sizeof(arr2) / sizeof(Number));

return 0;
}

纯虚函数

什么是纯虚函数

< 1 > 将成员函数声明为virtual

< 2 >该函数没有函数体(后跟=0)

如:

1
2
3
4
5
class CBank
{
public:
virtual double(返回值) GetAnnualRate(函数名)()(参数列表) = 0;
}

抽象类

< 1 >含有纯虚函数的类,称为抽象类(Abstract Class)

< 2 >抽象类也可以包含普通成员函数

< 3 >抽象类不能实例化

如使用以下方式定义一个对象:

1
2
CBank bank;						//全局区	栈
CBank* pBank = new CBank; //堆

此时编译器会报错cannot instantiate abstract class due to folowing members(无法实例化一个抽象类)

纯虚函数导致了抽象类的存在,抽象类则可以定义规范、定义一些规则
比如以下案例:

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
#include <stdio.h>
#include <windows.h>

// 银行年利率
class CBank
{
public:
virtual double GetAnnualRate() = 0;
};

// 中国工商银行
class ICBCBank :public CBank
{
private:
double m_dPrincipal;
public:
ICBCBank(double dPrincipal)
{
m_dPrincipal = dPrincipal;
}
double GetAnnualRate()
{
return 0.010;
}
double GetTotalMoney()
{
return m_dPrincipal + m_dPrincipal * GetAnnualRate();
}
};

// 中国建设银行
class CCBank :public CBank
{
private:
double m_dPrincipal;
public:
CCBank(double dPrincipal)
{
m_dPrincipal = dPrincipal;
}
double GetAnnualRate()
{
return 0.015;
}
double GetTotalMoney()
{
return m_dPrincipal + m_dPrincipal * GetAnnualRate();
}
};

void ShowAnnualRate(CBank* pBank[], DWORD nLength)
{
for (int i = 0; i < nLength; i++)
{
printf("%.5lf \n", pBank[i]->GetAnnualRate());
}
}

int main(void)
{
ICBCBank icbc(10000.0);
double dMoney1 = icbc.GetTotalMoney();
CCBank ccb(10000.0);
double dMoney2 = ccb.GetTotalMoney();

CBank* pBank[] = { &icbc,&ccb };
ShowAnnualRate(pBank, 2);

return 0;
}

在这个案例中,银行都继承了CBank这个抽象类,这个抽象类中的纯虚函数定义了子类应该如何获取年利率,因此子类需要重写这个函数。

而它存在的意义就是,当我们需要获取所有的银行的年利率时,有一个这样统一的规范可以将所有银行的年利率统一起来,使用void ShowAnnualRate(CBank* pBank[], DWORD nLength)来让父类指向子类。

这都是站在使用的角度来说。

对象拷贝-拷贝构造函数

拷贝构造函数

image-20240229083940206

拷贝构造函数时,如果子类有继承的父类对象,父类也会被继承过来

image-20240229085147294

浅拷贝

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 <stdio.h>
#include <string.h>

class Object
{
private:
int m_nLength;
char* m_strBuffer;
public:
Object() {}
Object(const char* str)
{
m_nLength = strlen(str) + 1;
m_strBuffer = new char[m_nLength];
memset(m_strBuffer, 0, m_nLength);
strcpy(m_strBuffer, str);
}
~Object()
{
delete[] m_strBuffer;
}
};

int main(void)
{
Object Obj("Hello World");
Object newObj(Obj);

return 0;
}

像这样拷贝一个对象,在类中传递了一个指针,当拷贝这个指针时,不会拷贝指针指向地址中存储的数据,而是会拷贝这个指针。

所以当原对象空间被释放时,会把后来拷贝的对象的空间一块释放掉,导致程序错误

这个程序拷贝完之后,发现拷贝后类中的字符串指向的地址是相同的

1
2
3
newObj	{m_nLength=12 m_strBuffer=0x00c8a640 "Hello World" }	Object

Obj {m_nLength=12 m_strBuffer=0x00c8a640 "Hello World" } Object

这两个字符串指向的是同一个地址0x00c8a640,当第一个对象地址释放时,拷贝的对象中的字符串也会被释放。

深拷贝

基于上面这种情况,要自己写一个拷贝构造函数。

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
#include <stdio.h>
#include <string.h>
class Object
{
private:
int m_nLength;
char* m_strBuffer;
public:
Object() {}
Object(const char* str)
{
m_nLength = strlen(str) + 1;
m_strBuffer = new char[m_nLength];
memset(m_strBuffer, 0, m_nLength);
strcpy(m_strBuffer, str);
}
Object(const Object& obj) //这里是拷贝构造函数,传入的参数必须是和类相同的对象类型。参数必须是Object&
{
m_nLength = obj.m_nLength;
m_strBuffer = new char[m_nLength]; //开辟了一块新的空间存储拷贝的值
memset(m_strBuffer, 0, m_nLength);
strcpy(m_strBuffer, obj.m_strBuffer);
}
~Object()
{
delete[] m_strBuffer;
}
};

int main(void)
{
Object Obj("Hello World");
Object newObj(Obj);

return 0;
}

这时候再看拷贝后的地址

1
2
Obj	{m_nLength=12 m_strBuffer=0x013ca640 "Hello World" }	Object
newObj {m_nLength=12 m_strBuffer=0x013ca678 "Hello World" } Object

一个是0x013ca640,一个是0x013ca678,这样当第一个释放空间的时候,第二个拷贝的对象不受影响。

内部类

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
#include <stdio.h>

class OutClass
{
private:
int a;
int b;

public:
class InnerClassPrivate
{
private:
int x;
int y;
public:
InnerClassPrivate(){}
InnerClassPrivate(int x, int y)
{
this->x = x;
this->y = y;
}
};
OutClass() {}
OutClass(int a, int b)
{
this->a = a;
this->b = b;
}
void Fn()
{
InnerClassPrivate In(1, 2);
}
};

int main(void)
{
OutClass Out(5, 6);
OutClass::InnerClassPrivate(7, 8);

return 0;
}

内部类就是定义在一个类内部的类,可以将内部类定义在外部类的public中,从外部使用OutClass::InnerCLass();可以进行访问。

如果内部类只在这个外部类中使用,则可以定义在这个类的private中,让外部无法访问或者也可以提供使用函数。内部类也可以定义在函数中,比如定义在OutClass的Fn()中。

内部类不会占用外部类的空间。外部类有int a; int b;,其中内部类中有int x,则外部类的大小仍然是8字节。

namespace命名空间

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
#include <stdio.h>

namespace ns1 {
int a;
int Fn(int x)
{
return x + 1;
}
class NameSpace
{
private:
int i;
int j;
NameSpace() {}
NameSpace(int i, int j)
{
this->i = i;
this->j = j;
}
};
}

namespace ns2 {
int a;
int Fn(int x)
{
return x + 1;
}
class NameSpace
{
private:
int i;
int j;
NameSpace() {}
NameSpace(int i, int j)
{
this->i = i;
this->j = j;
}
};
}

int main(void)
{
ns1::a = 0;
printf("%d\n", ns2::Fn(8));

return 0;
}

命名空间中的变量、函数、类等的名称可以相同,当调用时,用namespace::成员名来使用。

如果是创建了一个类文件,在头文件中声明后,可以用using namespace 命名空间来让整个程序直接使用指定命名空间中的成员。

static关键字

含义

1.

static关键字修饰的变量相当于一个私有的全局变量

比如有这样的一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
`class staClass
{
private:
public :
static int a;
int b;
int c;
staClass() {}
staClass(int b, int c)
{
this->b = b;
this->c = c;

}
};

其中变量a用static修饰,所以在staClass这个类中,a是一个属于staClass私有的全局变量,可以直接进行访问,但是不能从外部访问。

2.

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
#include <stdio.h>

class Demo
{
public :
static int z;
int a;
int b;
Demo() {}
Demo(int a, int b)
{
this->a = a;
this->b = b;
}
int SetZ(int z)
{
this->z = z;
return z;
}
int GetZ()
{
return z;
}
};

int Demo::z;

int main(void)
{
Demo d1(1, 2);
Demo d2(1, 2);

int temp = d1.SetZ(5);
int temp2 = d2.GetZ();

return 0;

}

这段代码中,int z用static修饰,所以z属于这个类中的全局变量,当使用d1对象访问和使用d2对象访问时,访问的是同一个z的地址。

static单子模式

当要限制创建一个对象时,可以使用static函数进行限制。

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
#include <stdio.h>

class One
{
private:
One() {} //1.首先要将无参构造函数私有,使从外部不能直接创建对象
static One* SetUpPoint; //3.创建一个静态成员,类型是One指针类型
public:
static One* Setup() //2.提供一个静态函数,因为static修饰的函数从外部可以被访问
{
if (SetUpPoint == NULL) //5.当这个指针是空指针时,就创建一个对象
SetUpPoint = new One();
return SetUpPoint; //6.将创建的这个指针返回
}
};

One* One::SetUpPoint = NULL; //4.初始化静态成员为空指针

int main(void)
{
One* p1 = One::Setup();
One* p2 = One::Setup();

return 0;
}

面向对象设计中的static之静态成员函数:

总结:

1
2
3
4
5
6
7
<1>出现在类体外的函数定义不能指定关键字static;
<2>静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
<3>非静态成员函数可以任意地访问静态成员函数和静态数据成员;
<4>静态成员函数不能访问非静态成员函数和非静态数据成员;
<5>调用类的静态成员函数的两种方式;
<类名>::<静态成员函数名>(<参数表>)
<对象名>.<静态成员函数名>(<参数表>)