前序知识

变量:给一段指定的内存空间起名,方便操作这段内存。

C++定义常量的两种方式:

  1. #define宏常量:#define 常量名 常量值
  2. const修饰的变量:const 数据类型 常量名 = 常量值

标识符的命名规则:

  • 标识符不能是关键字
  • 标识符只能由字母、数字、下划线组成
  • 第一个字符必须为字母或下划线
  • 标识符字母区分大小写

1 初识知识

1.1数据类型

数据类型存在的意义:给变量分配合适的内存空间

1.1.1整型

image-20240229211305122

1.1.2浮点型

image-20240229211916435

1.1.3字符型

字符型变量用于显示单个字符

语法:char ch = 'a'

注意:

  • C和C++中字符型变量只占用1个字节
  • 字符型变量并不是把字符本身放到内存中进行存储,而是将对应的ASCII码存储

1.1.4转义字符

用于表示一些不能显示出来的ASCII字符

image-20240229212521459

1.1.5字符串

C风格字符串:char str[ ] = "abcdefg"

C++风格字符串:string str = "abcdefg"

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>
//注意,C++风格要加string头文件
using namespace std;
int main()
{
char str1[]="abcd";
string str2="abcd";
return 0;
}

1.1.6布尔类型

作用:布尔数据类型代表真或者假的值

bool占用1个字节的大小

1.1.7数据的输入和输出

关键字:cin、cout

语法:cin >> 变量名cout << 变量名

1.1.8程序的流程结构

C/C++支持最基本的三种程序运行结构:==顺序结构==、==选择结构==、==循环结构==

1.1.9一维数组

一维素组定义的三种方式:

  • int a[10]={1,2,3,4}
  • int a[]={1,2,3,4}

1.1.10一维数组-冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
int main()
{
int a[] = {4,5,3,10,2,8,32};
//寻找n轮最大值
for (int i = 0; i < 7; i++)
{
//每一轮进行n-i-1次判断和交换,得出该轮的最大值
for (int j = 0; j < 7-i-1; j++)
{
if (a[j] < a[j + 1])
{
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
for (int i = 0; i < 7; i++)cout << a[i] << '\t';
return 0;
}
  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素做同样的工作,执行完毕后,找到第一个最大值。
  • 重复上面的步骤,每次比较数-1,直到不需要比较。
img

1.1.11二维数组

定义方式:

int a[2][2]={{数据1,数据2},{数据3,数据4}}

int a[2][2]={数据1,数据2,数据3,数据4}

int a[][2]={数据1,数据2,数据3,数据4}

注意:必须要指明列数,行数可以无需指明

*1.1.12函数的分文件编写

步骤:

  • 创建后缀名为.h的头文件,在头文件中写函数声明
  • 创建后缀名为.cpp的源文件,在源文件中写函数的实现

main文件

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include "swap.h"
using namespace std;
int main()
{
int a=0,b=1;
swap(&a,&b);
cout<<a<<b<<endl;
return 0;
}

swap.h

1
void swap(int *a,int *b);

swap.cpp

1
2
3
4
5
6
7
#include "swap.h"
void swap(int *a,int *b)
{
int temp=*a;
*a=*b;
*b=temp;
}

1.1.13指针相关

指针的本质就是一个十六进制的数,用于表示地址,在32位下占用4字节,64位下占用8字节

空指针:指向编号为0的空间,用于初始化指针变量,int *a = NULL

注意:空指针是==不可访问的==,0~255之间的编号是系统占用的,因此不可访问

野指针:指向非法的内存空间(不是自己申请的空间),int *p=(int *)0x1100

const修饰指针

  1. const修饰指针:常量指针const int * p
  2. const修饰常量:指针常量 int * const p
  3. const既修饰指针,又修饰常量const int * const p
1
2
3
4
const int *p = &a; //常量指针
p = &b;
*p = 20;//这个操作是错误的
//特点:指针指向可以改,但指针指向的值不可以改
1
2
3
4
int * const p = &a;//指针常量
p = &b;//这个操作是错误的
*p = 20;
//特点:指针的指向不可以改,但指针指向的值可以改
1
2
const int* const p=&a;
//特点:指针指向和值都不可以改

看const位置,只要const紧跟的就可以修改,可以理解为锁定变量指针

1.1.14结构体

语法:struct 结构体名 {成员列表}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct student
{
string name;
int age;
int score;
};

int main()
{
struct student Stu1={"ZhangSan",18,90};
//C++中结构体创建struct关键字可以省略
student Stu2 = {"LiSi",18,80};
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct student
{
string name;
int age;
int score;
}Stu;//顺便定义

int main()
{
Stu.name="ZhangSan";
Stu.age=18;
Stu.score=90;
return 0;
}

1.2 climits头文件

定义了各种整型数据的最大值和最小值

1
#include <climits>
image-20240305105400635
image-20240305105423398
1
#define INT_MAX 2147483647

头文件中有许多如上的指令

#define也是一个预处理编译指令,告诉编译器将INT_MAX替换为2147483647

2 C++核心编程

2.1内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:用于存放函数的二进制代码,由操作系统进行管理
  • 全局区:存放全局变量和静态变量(static)以及常量(const)
  • 栈区:由编译器自动分配释放,存放函数参数和局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束由操作系统回收

内存分区的意义:不同区域存放的数据,赋予不同的生命周期,更为灵活的编程

代码区和全局区都是在程序运行前就存在,栈区和堆区都在程序运行时才会分配

2.1.1堆区开辟和释放

1
2
3
4
int * p1 = new int(10);//p1指向一个值为10的int数
int * p2 = new int[20];//开辟20长度的数组空间
int * p3 = (int *)malloc(20*sizeof(int));
//指针p1,p2,p3本质上还是一个局部变量,放在栈上的,但指针指向的空间在堆区
1
2
delete(p1);
free(p2);

2.2引用&

本质就是指针常量(int* const ref = &a),指向不可改变,值可改

1
2
int a=10;
int &b=a;

注意,b并不是指针,是对a的引用,给一个变量起别名

区别就在于,引用必须初始化,创建后不可以改变指向,指针可以修改指向

注意:引用可以用作函数返回值,但是不要返回局部变量的引用,因为在函数结束后,栈区会释放。

2.3函数提高

2.3.1函数形参

在C++中,函数的形参列表可以设置默认值

1
2
3
4
5
int func(int a,int b = 10, int c=20)
{
//默认值,如果没有传入b/c就按默认值来
//注意:此时的a必须传入,否则会报错
}

如果某个位置已经有了默认参数,那么从这个位置往后,都必须有默认参数

1
int func(int a,int b =10, int c)//这种情况会报错

函数的声明和实现只能一个有默认参数,不能同时有

1
2
3
4
5
int func(int a,int b=10,int c=20);//声明
int func(int a, int b,int c){ }//实现

int func(int a, int b,int c);//声明
int func(int a, int b=10, int c=20){ }//实现

2.3.2占位参数

1
void func(int a, int){ }

调用函数时必须填补该参数

1
func(1,100);//100即填充,可以填任意数

2.3.3函数重载

作用:函数名可以相同,提高复用性

  • 同一个作用域下,函数名相同
  • 函数参数 类型不同个数不同顺序不同
1
2
3
4
5
6
7
8
void func(int a)
{
cout<<"int函数:"<<a<<endl;
}
void func(double a)
{
cout<<"double函数:"<<a<<endl;
}

注意:

函数重载碰到默认参数时,默认参数是不计的

1
2
3
int func(int a,int b=10,int c=20){ }
int func(int a);
//上面代码是错误的,无法发生函数重载

函数重载碰到引用时,

1
2
3
void func(int &a)//调用func(a)变量

void func(const int &a)//调用func(10)常量

2.4类和对象

C++面向对象的三大特征:封装继承多态

万事万物皆为对象,对象上有其属性和行为

  • struct 默认权限为公共 public
  • class 默认权限为私有 private

2.4.1封装

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>
using namespace std;
class circle
{
public://访问权限:公共权限
double r;
const double PI = 3.1415926;//属性

double calculateZC()//行为
{
return 2 * PI * r;
}
};//注意这里的分号
int main()
{
circle c1;//创建对象(实例化)
c1.r = 1.0;//赋值
cout << c1.calculateZC() << endl;
return 0;
}

2.4.2权限

  • public 公共权限
  • protected 保护权限
  • private 私有权限(默认)
1
2
3
4
5
6
7
8
9
10
11
12
//公共权限	public		类内可以访问,类外也可以访问
//保护权限 protected 类内可以访问,类外不可以访问(子类可以访问)
//私有权限 private 类内可以访问,类外不可以访问(子类不能访问)
class Person
{
public:
string name;
protected:
string id;
private:
int password;
};

类中成员的默认权限为private

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 <iostream>
#include <string>
using namespace std;
class Cube
{
private:
double l, w, h;
public:
void setLWH(double ll, double ww, double hh)
{
l = ll;
w = ww;
h = hh;
}
double getV()
{
return l * w * h;
}
double getA()
{
return 2 * (l * w + w * h + l * h);
}
bool ifequ(Cube x)
{
return x.l == l && x.h == h && x.w == w ? true : false;
}
};
int main()
{
Cube a,b;
a.setLWH(1, 2, 3);
b.setLWH(1, 2, 3);
cout << "面积为:" << a.getA() << "体积为:" << a.getV() << endl;
cout << "是否相等" << a.ifequ(b) << endl;
return 0;
}

2.4.3对象的初始化和清理(构造和析构)

如果我们不提供构造函数和析构函数,编译器会提供空实现的构造函数和析构函数

  • 构造函数:主要作用在于创建对象时为对象成员属性赋值,构造函数由编译器自动调用,无需手动调用
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作

构造函数可以有参数,可以发生重载

析构函数不能有参数,不能发生重载

1
2
3
4
5
6
7
8
9
10
11
12
class Person
{
public:
Person()//构造函数
{

}
~Person()//析构函数
{

}
};

2.4.3.1构造函数的分类及调用

分类:有参构造、无参构造(普通构造)/拷贝构造

调用:括号法、显示法、隐式转换法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person
{
public:
int age;
Person()//普通构造
{
cout<<"无参构造函数调用"<<endl;
}
Person(int a)//普通构造
{
age = a;
cout<<"有参构造函数调用"<<endl;
}
Person(const Person &p )//拷贝构造
{
age = p.age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void test()
{
1、括号法
Person p1;//默认构造函数,不需要加(),如果加了()编译器会认为是一个函数声明
Person p2(10);//有参构造函数
Person p3(p2);//拷贝构造函数

2、显示法
Person p2 = Person(10);
Person p3 = Person(p2);
补充:Person(10);//匿名对象 特点:当前行执行结束就回收

3、隐式转换法
Person p4 = 10;
Person p5 = p4;
}

2.4.3.2拷贝构造函数调用时机

  • 使用一个已经创建完毕的对象来初始化新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
1
2
3
4
5
void test01()
{
Person p;
Person p1(p);
}
1
2
3
4
5
6
7
8
void doWork(Person p)
{
}
void test02()
{
Person p;
doWork(p);
}
1
2
3
4
5
6
7
8
9
Person doWork()
{
Person p3;
return p3;
}
void test03()
{
Person p = doWork();
}

2.4.3.3构造函数的调用规则

默认的拷贝构造函数会进行值拷贝,其他的构造函数都是空实现的

  • 如果用户定义了有参构造函数,C++不再提供默认的无参构造函数,但会提供默认的拷贝构造
  • 如果用户定义了拷贝构造函数,C++不会再提供其他构造函数

2.4.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
#include <iostream>
#include <string>
using namespace std;
class Stu
{
public:
string name;
int* m_height;
Stu(string a, int height)
{
name = a;
m_height = new int(height);
}
~Stu()
{
if (m_height != NULL)
{
delete(m_height);
m_height = NULL;
}
}
};
int main()
{
Stu c1("vicczyq", 172);
Stu c2(c1);
return 0;
}

这段代码,c2会拷贝c1的值,但是他拷贝的是指针,是同一个地址,当c2先析构后,c1析构时候释放内存会出错崩溃

image-20240301224532559

因此,在进行拷贝时,要新申请一个空间,这样才能解决

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
#include <iostream>
#include <string>
using namespace std;
class Stu
{
public:
string name;
int* m_height;
Stu(string a, int height)
{
name = a;
m_height = new int(height);
}
Stu(const Stu &p)
{
//m_height = p.m_height;//浅拷贝(不会申请新的空间)
m_height = new int(*p.m_height);//深拷贝(申请新空间)
name = p.name;
}
~Stu()
{
if (m_height != NULL)
{
delete(m_height);
m_height = NULL;
}
}
};
int main()
{
Stu c1("vicczyq", 172);
Stu c2(c1);
return 0;
}

2.4.3.5初始化列表

1
2
3
4
5
6
7
8
9
class Person
{
public:
int m_a,m_b,m_c;
Person(int a, int b, int c) : m_a(a),m_b(b),m_c(c)
{

}
};

2.4.3.6类对象作为类成员

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 <iostream>
#include <string>
using namespace std;
class A
{
public:
A()
{
cout << "A构造" << endl;
}
};
class B
{
A a;
public:
B()
{
cout << "B构造" << endl;
}
};
int main()
{
B x;
return 0;
}
image-20240301230820745

证明,构造B时,需要先完成A的构造

2.4.3.7 static静态成员变量

  • 所有对象都共享同一份数据。

  • 编译阶段就会进行内存分配(全局区)

  • 类内声明,类外进行初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
public:
static int m_A;//类内声明
}
int Person::m_A = 100;//类外初始化

void test()
{
Person p1;
cout<< p1.m_A <<endl;//输出100
Person p2;
p2.m_A=200;
cout << p1.m_A << endl;//访问的任然是p1,但输出200
}

静态成员变量不属于任何一个对象,所有对象都共享一份数据

所以可以通过两种方式进行访问:

1
2
3
Person p1;
cout << p1.m_A << endl;//创建对象进行访问
cout << Person::m_A <<endl;//通过类名进行访问

2.4.3.8 static静态成员函数

所有对象都共享一个函数,并且函数只能访问静态成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
public:
static int m_A;

static void func()
{
cout << "static void func调用" << endl;
cout << m_A << endl; //只能访问静态成员变量
}
}
int Person::m_A = 100;

void test()
{
Person p1;
p1.func();//通过对象访问

Person::func();//通过类名访问
}

2.4.4 C++对象模型和this指针

2.4.4.1 成员变量和成员函数分开存储

注意:静态成员变量或函数不属于任何一个对象上。

空对象占用的内存空间大小为:1

只有非静态成员变量属于类的对象上,

其他(静态成员变量、成员函数)都不属于类的对象上,也就是说只有一份。

2.4.4.2 this指针

由2.4.4.1可知,多个对象会共享(静态成员变量、静态成员函数和非静态成员函数)。

可以通过this指针区分对象调用

this指针指向被调用的成员函数所属的对象

用途:

  • 当函数形参和成员变量同名时,可以用this来区分
  • 当类的非静态成员函数返回对象本身时,可以用return *this
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person
{
public:
string name;
void setName(string name)
{
this->name = name;
}
Person getInfo()
{
return *this;
}
}

2.4.5 const修饰成员函数(常函数)

  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

对象创建时,加const称为常对象,常对象只能调用常函数。

主要是对函数内this指针的限制

1
2
3
4
5
6
7
8
9
10
11
12
class Person
{
public:
void showPerson() const
{
this -> m_A = 100;//报错,const修饰this指向的值不可修改
this -> m_B = 100;//有mutable关键字可修改
}

int m_A;
mutable int m_B;
}

2.4.6 友元 friend

让一个函数或者类 访问另一个类中的私有成员

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
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 <iostream>
using namespace std;
class Building{
friend void goodGay(Building* building);//声明友元函数
public:
string m_SittingRoom;//客厅
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
private:
string m_BedRoom;//卧室
};

void goodGay(Building *building)
{
cout << "Gay:" << building->m_SittingRoom << endl;
cout << "Gay:" << building->m_BedRoom << endl;
}
void main()
{
Building a;
goodGay(&a);
}
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 <iostream>
#include <string>
using namespace std;
class Building {
friend class GoodGay;
public:
string m_SittingRoom;//客厅
Building()
{
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
private:
string m_BedRoom;
};

class GoodGay {
public:
Building* building = new Building;
void visit();//函数声明,可以在类内实现,也可以在类外
};

void GoodGay::visit()//类外实现成员函数
{
cout << "Gay:" << building->m_SittingRoom << endl;
cout << "Gay:" << building->m_BedRoom << endl;
}

void main()
{
GoodGay gay;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>
using namespace std;
class Home;
class GoodGay {
public:
void func(Home* home);
};
class Home {
friend void GoodGay::func(Home * home);
private:
int id = 10;
};

void GoodGay::func(Home* home)
{
cout << home->id << endl;
}
void main()
{
GoodGay a;
Home home;
a.func(&home);
}

2.4.7 运算符重载

对于内置的数据类型,编译器知道怎么进行数据运算

但是对于其他的数据类型,需要自己定义计算方法

2.4.7.1 加号运算重载

1
Person p3 = p1+p2

通过成员函数重载”+“

1
2
3
4
5
6
7
Person operator+ (Person &p)
{
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}

通过全局函数重载”+“

1
2
3
4
5
6
7
Person operator+ (Person &p1, Person &p2)
{
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}

2.4.7.2 左移运算符重载

作用:自定义输出内容

只能用全局函数进行重载

1
2
3
4
5
ostream & operator<<(ostream &cout,Person p)
{
cout << p.m_A;
return cout;
}

2.4.7.3 递增/减运算符重载

作用:通过重载递增运算符,实现自己的整型数据

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 <iostream>
#include <string>
using namespace std;
class MyInt {
friend ostream & operator<<(ostream& out, MyInt& p);
public:
//重载前置++运算符
//返回引用的原因是要对同一个数据空间进行操作
//如果不用引用,会复制出一个副本
MyInt &operator++()
{
i++;
return *this;
}
//重载后置++运算符
MyInt operator++(int)//返回的是值,因为temp在函数结束后就会释放
{
MyInt temp = *this;
i++;
return temp;
}
private:
string name = "vicczyq";
int i = 10;
};

ostream & operator<<(ostream &out, MyInt &p)
{
out << p.name << p.i;
return out;
}
void main()
{
MyInt a;
cout << a << endl;
++(++a);
cout << a << endl;
}

2.4.7.4 赋值运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
public:
int *m_age;
Person(int x)
{
m_age = new int(x);
}

Person & operator=(Person & p)
{
if(m_age!=NULL)
{
delete m_age;
m_age = NULL;
}
m_age = new int(p.m_age);//深拷贝
return *this;
}
}

2.4.7.5 关系运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
{
public:
string name;
int id;
bool operator==(Person &p)
{
if(p.name==name && p.id==id)
{
return true;
}
else return false;
}
}

2.4.8 继承

下级别成员拥有上一级的共性,又有自己的特点。减少重复代码

image-20240303191037932

2.4.8.1 继承的基本语法

基本语法:class 子类 : 继承方式 父类

子类 也称为 派生类

父类 也称为 基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
using namespace std;

class BasePage {
public:
void Print()
{
cout << "父类的打印函数" << endl;
}
};
class Page1 : public BasePage //此处实现继承操作,注意是一个冒号
{

};
void main()
{
Page1 p;
p.Print();//子类对象访问父类的方法
}

2.4.8.2继承方式

1
2
3
4
5
6
7
8
9
class A
{
public:
int a;
protected:
int b;
private:
int c;
};
  • 公共继承

    1
    2
    3
    4
    5
    6
    7
    class B : public A
    {
    public:
    int a;
    protected:
    int b;
    };
  • 保护继承

    1
    2
    3
    4
    5
    6
    class C : protected A
    {
    protected:
    int a;
    int b;
    }
  • 私有继承

    1
    2
    3
    4
    5
    6
    class D : private A
    {
    private:
    int a;
    int b;
    }

所谓的继承方式就是继承成为对应权限,除了公共继承需要考虑protected的处理。

注意:父类的private不管怎么样都是不能继承的

2.4.8.3 继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BasePage {
public:
int a;
protected:
int b;
private:
int c;
};
class Page1 : public BasePage
{

};
void main()
{
Page1 p;
cout << sizeof(p) << endl;
//此处会输出12,因此继承了a,b,c但是c被隐藏
}

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

2.4.8.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>
#include <string>
using namespace std;
class BasePage {
public:
BasePage()
{
cout << "父类构造函数" << endl;
}
~BasePage()
{
cout << "父类析构函数" << endl;
}
};
class Page1 : public BasePage
{
public:
Page1()
{
cout << "子类构造函数" << endl;
}
~Page1()
{
cout << "子类析构函数" << endl;
}
};
void main()
{
Page1 p;
}

先调用父类的构造函数,再调用子类的构造函数

先调用子类的析构函数,再调用父类的析构函数

image-20240303195651224

2.4.8.5 同名成员处理方式

  • 访问子类中的同名成员:直接访问即可
  • 访问父类中的同名成员:需要添加作用域
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
#include <iostream>
#include <string>
using namespace std;

class BasePage {
public:
string str="父类";
void func()
{
cout << "同名成员函数:父类" << endl;
}
};
class Page1 : public BasePage
{
public:
string str="子类";
void func()
{
cout << "同名成员函数:子类" << endl;
}
};
void main()
{
Page1 p;
cout << p.str << endl;
cout << p.BasePage::str << endl;//父类作用域
p.func();
p.BasePage::func();//父类作用域访问同名成员函数
}

2.4.8.6 多继承语法

语法:`class 子类 : 继承方式 父类1, 继承方式 父类2 ...

多继承可能会引发父类中同名成员的出现,需要加作用域进行区分,

C++实际开发中不建议用多继承

2.4.8.7 菱形继承/虚继承

两个类继承于同一个父类,又有某个类同时集成于这两个类

img

由上图可知,羊驼继承了动物的数据两次,我们真实情况只需要一个数据就行

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal{
public:
int m_Age;
};
class Sheep : public Animal{};
class Tuo : public Animal{};
class SheepTuo : public Sheep, public Tuo{};
void main()
{
SheepTuo st;
st.Sheep::m_Age = 18;
st.Tuo::m_Age = 28;//会出现两个情况
}

虚继承即可解决

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal{
public:
int m_Age;
};
class Sheep :virtual public Animal{};
class Tuo :virtual public Animal{};//利用virtual关键字实现虚继承
class SheepTuo : public Sheep, public Tuo{};
void main()
{
SheepTuo st;
st.Sheep::m_Age = 18;//此时不管访问Sheep还是Tuo都是18
st.Tuo::m_Age = 28;//此时不管访问Sheep还是Tuo都是28
}

2.4.9 多态

对于同一个函数,传入不同的参数会反应出不同的状态,即执行不同的内容。

  • 静态多态:函数重载 和 运算符重载
  • 动态多态: 子类和虚函数实现运行时的多态

静态多态和动态多态的区别:

  • 静态多态:编译阶段确定函数地址(早绑定)
  • 动态多态:运行阶段确定函数地址(晚绑定)
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
class Animal{
public:
void speak()
{
cout<<"动物正在说话"<<endl;
}
};
class Cat : public Animal{
public:
void speak()
{
cout<<"小猫在说话"<<endl;
}
};
//地址进行了早绑定,在编译阶段确定了函数地址,
//输出内容为:“动物在说话”
void doSpeak(Animal & animal)
{
animal.speak();
}
void main()
{
Cat cat;
doSpeak(cat);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal{
public:
virtual void speak()//通过virtual,虚函数实现动态多态
{
cout<<"动物正在说话"<<endl;
}
};
class Cat : public Animal{
public:
void speak()
{
cout<<"小猫在说话"<<endl;
}
};
//此时输出“小猫在说话”
void doSpeak(Animal & animal)
{
animal.speak();
}
void main()
{
Cat cat;
doSpeak(cat);
}

动态多态满足条件:

  1. 有继承关系
  2. 子类要重写父类的虚函数

重写:函数名、参数、返回值类型都必须相同。

重载:函数名相同,参数、返回值类型可能不同。

2.4.9.1多态的原理剖析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal
{
void doSpeak()
{
cout << "动物正在说话" << endl;
}
};
此时sizeof(Animal)为1
class Animal
{
void virtual doSpeak()
{
cout << "动物正在说话" << endl;
}
};
此时的sizeof(Animal)为4

4个字节的原因是指针占用。

virtual关键字会形成一个vfptr指针(虚函数表指针)virtual function pointer

image-20240304194459436

当子类继承时,会重写doSpeak函数,vfptr的指针指向的地址就会发生改变。

image-20240304195120122

在每次调用时候,会直接调用当前类的函数,所以可以实现多态。

2.4.9.2 纯虚函数和抽象类

通常发生多态后,父类中的函数就没什么意义,主要是用于调用子类重写内容,

因此可以将虚函数改为纯虚函数

语法:virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类就称为抽象类

抽象类特点:无法实例化对象,子类必须重写抽象类中的纯虚函数,否则也属于抽象类

1
2
3
4
5
6
7
8
9
10
11
12
class Animal{//抽象类
public:
virtual void doSpeak() = 0;//纯虚函数
};

class Cat : public Animal{
public:
void doSpeak()
{
cout<<"小猫在说话"<<endl;
}
};

2.4.9.3 虚析构和纯虚析构

多态使用时,如果子类中有开辟到堆区的数据,父类指针在释放时无法调用到子类的析构代码

父类指针在进行析构的时候,不会调用子类中的析构函数,导致子类的堆区数据,出现内存泄漏。

采用 虚析构纯虚析构 即可解决。

1
2
3
4
5
6
7
8
9
10
11
12
//虚析构
virtual ~类名(){
//虚析构实现
}

//纯虚析构,属于抽象类
virtual ~类名() = 0;
/*全局作用域*/
Animal::~Animal()
{
//纯虚析构实现
}

2.5文件操作

C++对文件操作需要包含头文件<fstream>

文本文件以ASCII码形式存储在计算机中,二进制文件以二进制形式存储

三大类:

  • ofstream:写操作
  • ifstream:读操作
  • fstream:读写操作

2.5.1 文本文件

打开方式:

image-20240304204659205

2.5.1.1 写文件

1
2
3
4
5
6
7
8
9
#include <fstream>
int main()
{
ofstream ofs;
ofs.open("文件路径", ios::out);
ofs << "写入的数据";
ofs.close();
return 0;
}

文件打开方式可以配合使用,如ios::out | ios::binary就是以二进制形式写出

2.5.1.2 读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fstream>
#include <iostream>
int main()
{
ifsteam ifs;
ifs.open("文件路径", ios::in);

if(!ifs.isopen())
{
cout<<"文件打开失败"<<endl;
return 0;
}

//读数据

ifs.close();
return 0;
}

读数据:

方法一:

1
2
3
4
5
char buf[1024] = {0};//可以用string
while( ifs >> buf )
{
cout << buf << endl;
}

方法二:

1
2
3
4
5
char buf[1024] = {0};//可以用string
while( ifs.getline(buf),1024 )//或者用sizeof
{
cout << buf << endl;
}

方法三:

1
2
3
4
5
string buf;
while( getline(ifs, buf))
{
cout << buf;
}

方法四:

1
2
3
4
5
char c;
while( (c=ifs.get()) != EOF )
{
cout << c;
}

总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <fstream>
#include <iostream>
using namespace std;
int main()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "张三" << endl;
ofs << "18岁" << endl;
ofs << "男" << endl;
ofs.close();

ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open())return 0;
string str;
while ( ifs>>str )
{
cout << str<<endl;
}
ifs.close();
return 0;
}

2.5.2 二进制文件

打开方式需要添加ios::binary

2.5.2.1 写文件

1
2
3
4
5
Person p = {"张三",18};//列表初始化,等效于p("张三",18)
ofstream ofs;
ofs.open("文件地址", ios::binary | ios::out);
ofs.write((const char*)p, sizeof(Person));
ofs.close();

2.5.2.1 读文件

1
2
3
4
5
6
7
8
Person p;
ifstream ifs;
ifs.open("文件地址", ios::binary | ios::in);

if(!ifs.is_open())return 0;

ifs.read((const char*)p, sizeof(Person));
ifs.close();

2.6 模板

2.6.1基本语法

1
2
3
4
5
6
7
8
9
10
11
12
void swapInt(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
void swapDouble(double &a, double &b)
{
double temp = a;
a = b;
b = temp;
}

数据类型不同,但是代码逻辑是相同的,采用模板可以改写为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>//声明一个模板,告诉编译器后面的T是一个通用的数据类型
//typename可以用class代替
void Myswap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}

void main()
{
int a=10,b=20;
Myswap(a,b);//自动类型推导
Myswap<int>(a,b);//手动指定数据类型
}

模板必须要确定出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
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
using namespace std;
template <class T>
void Swap_element(T &a,T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <class T>
void SelectSort(T &arr)
{
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++)
{
int maxFlag = i;
for (int j = i + 1; j < len; j++)
{
if (arr[j] > arr[maxFlag])
{
maxFlag = j;
}
}
if (maxFlag != i)
{
Swap_element(arr[maxFlag], arr[i]);
}
}
for (int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
}

int main()
{
int a[] = { 2,3,4,1,0,2,9,10 };
SelectSort(a);
char str[] = "gfbseqa";
SelectSort(str);
return 0;
}

2.6.2 普通函数与函数模板的区别

普通函数可以发生隐式类型转换

函数模板(自动类型推导),不可以发生隐式类型转换

函数模板(手动指定类型),可以发生隐式类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int Add_pt(int &a,int &b){
return a+b;
}

template <typename T>
T Add_mb(T &a, T &b){
return a+b;
}

void main(){
int a=1,b=2;
char c=3;
Add_pt(a,c);//普通函数隐式类型转换,将c转为了int类型
Add_mb(a,c);//此处会报错,自动推导
add_mb<int>(a,c);//隐式类型转换
}

2.6.3 普通函数和函数模板调用规则

  • 如果普通函数和函数模板都可以实现,优先调用普通函数
  • 可以通过空模板参数列表来强制调用函数模板
  • 函数模板页也可以发生重载
  • 如果函数模板产生更好的匹配,优先调用函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Print(int a,int b){
cout<<"调用普通函数"<<endl;
}

template<class T>
void Print(T a,T b){
cout<<"调用模板函数"<<endl;
}

template<typename T>
void Print(T a,T b,T c){
cout<<"调用重载的函数模板"<<endl;
}
void main()
{
Print(1,1);//优先调用普通函数
Print<>(1,1);//强制调用函数模板
Print(1,1,1);//调用重载函数模板
Print('a','b');
//会优先调用函数模板,产生了更好的匹配,不需要进行类型转换(char->int)
}

2.6.4 模板局限性

模板并不是万能的,如果传入 a和b是一个数组,模板就无法实现

1
2
3
4
template <class T>
void f(T a, T b){
a = b;
}

如果传入的是像Person这样的自定义数据类型,也无法正常运行

1
2
3
4
template <class T>
bool f(T a,T b){
if(a > b){.....}
}

解决方法:1.运算符重载、2.具体化Person

1
2
3
4
template<> bool f(Person &p1, Person &p2){
if(p1.m_Name==p2.m_Name)return true;
else return false;
}

2.6.5 类模板

类模板中的成员函数只有在调用时候才会进行创建,普通类中的成员函数在一开始就创建在代码区

(可以记成,不同的一组typename就是不同的class,所以在外部访问的时候需要用<>)

2.6.5.1 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

template <typename NameType,typename AgeType>
class Person {
public:
NameType m_Name;
AgeType m_Age;
Person(NameType name, AgeType age){
m_Name = name;
m_Age = age;
}
};
int main()
{
Person<string,int> p("vicczyq",18);//必须手动指定类型
return 0;
}

2.6.5.2 类模板和函数模板的区别

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在木板参数列表中可以有默认参数
1
2
3
4
5
6
template <typename NameType, typename AgeType =int>
class Person{
public:
NameType m_Name;
AgeType m_Age;
};

2.6.5.3 类模板对象作函数参数

1
2
3
4
5
6
template <typename NameType, typename AgeType>
class Person{
public:
NameType m_Name;
AgeType m_Age;
};
  • 指定传入类型 --直接指定对象的数据类型
1
2
3
4
5
6
7
void printPerson(Person<string,int> &p){

}
void main(){
Person<string,int> p("vicczyq",18);
printPerson(p);
}
  • 参数模板化 --将对象中的参数变为模板进行传递
1
2
3
4
5
6
7
8
template<typename T1, typename T2>
void printPerson(Person<T1,T2> &p){

}
void main(){
Person<string,int> p("vicczyq",18);
printPerson(p);
}
  • 整个类模板化 --将这个对象类型模板化进行传递
1
2
3
4
5
6
7
8
9
template<typename T>
void printPerson(T &p){

}
void main()
{
Person<string,int> p("vicczyq",18);
printPerson(p);//利用模板函数自动类型推导
}

2.6.5.4 类模板继承

当父类是一个类模板时,子类继承需要指出父类中的数据类型

如果想灵活使用数据类型,子类也必须为类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename NameType,typename AgeType>
class Person{
public:
NameType m_Name;
AgeType m_Age;
}

class Son : public Person<string,int>{

}

template <typename T1, typename T2, typename T3>
class Son : public Person<T1,T2>
{
public:
T3 sex;
}

2.6.5.5 类模板成员函数类外实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename NameType>
class Person{
public:
NameType m_Name;
void getName();
Person();
};

template <typename T>
T Person<T>::getName()
{
return m_Name;
}

template <typename T>
Person<T>::Person(){

}

2.6.5.6 类模板份文件编写

由于类模板中成员函数创建时机是在调用阶段,导致份文件编写时候无法链接。

解决方法:

  • 解决方法1:直接包含cpp源文件

person.h

1
2
3
4
5
6
7
template <typename NameType>
class Person{
public:
NameType m_Name;
void getName();
Person();
};

person.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "person.h"

template <typename T>
T Person<T>::getName()
{
return m_Name;
}

template <typename T>
Person<T>::Person(){

}

main函数

1
2
3
4
#include "person.cpp"
void main(){
Person<string> p;
}
  • 解决方法2:将声明和实现写到同一个文件中,并更改后缀名为.hpp(.hpp只是约定名称,不是强制限制)相当于直接写在了.h文件中

main函数

1
2
3
4
#include "person.hpp"
void main(){
Person<string> p;
}

person.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename NameType>
class Person{
public:
NameType m_Name;
void getName();
Person();
};
template <typename T>
T Person<T>::getName()
{
return m_Name;
}

template <typename T>
Person<T>::Person(){

}

2.6.5.7 类模板与友元

1
2
3
4
5
6
7
8
9
10
11
template <typename NameType, typename AgeType>
class Person{
//其实是全局函数,友元函数直接在这里实现
friend void PrintInfo(Person<NameType,AgeType> &p)
{
cout << p.m_Name << p.m_Age;
}
private:
NameType m_Name;
AgeType m_Age;
};