c++ 面向对象/核心编程/c++入门

1 内存分区模型

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

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

1.1 程序运行前

在程序编译后,生成了 exe 可执行程序,未执行该程序前分为两个区域:

代码区:

存放 CPU 执行的机器指令,代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

全局区:

该区域的数据在程序结束后由操作系统释放


1.2 程序运行后

栈区:

由编译器自动分配释放,存放函数的参数值,局部变量等

注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

int * func()
{
    
    
	int a = 10;
	return &a;
}

int main()
{
    
    
	int* p = func();

	cout << *p << endl;
	cout << *p << endl;

	return 0;
}

堆区:

由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

主要利用​ new ​在堆区开辟内存

int* func()
{
    
    
	int* a = new int(10);
	return a;
}

int main()
{
    
    
	int* p = func();

	cout << *p << endl;
	cout << *p << endl;

	return 0;
}

堆区开辟的数据,由程序员手动开辟,手动释放,利用 delete 操作符释放

利用 new 创建的数据,会返回该数据对应的类型的指针


1.3 new 实现动态内存分配

  • 第一种用法:分配一个变量 P = new T;

  • 参数解释:T 是任意类型名,P 是 T* 类型的指针

  • 作用:动态分配一片大小为 sizeof(T) 字节的内存空间,并将该内存空间的起始地址赋值给 P

int* pn;
pn = new int;
*pn = 5;

  • 第二种用法:分配一个数组 P = new T[N];
  • 参数解释:T 是任意类型名,P 是 T* 类型的指针,N 是要分配的数组元素的个数,可以是整型表达式
  • 作用:动态分配一片大小为 N*sizeof(T) 字节的内存空间,并将该内存空间的起始地址赋值给 P
int* pn;
int i = 5;
pn = new int[i * 20];
pn[0] = 20;

  • 用 new 动态分配的内存空间,一定要用 delete 释放,否则会一直占用空间,从而可能会导致操作系统或其他应用程序的运行的内存空间都不够
delete 指针;  // 该指针必须指向 new 出来的空间

int* p = new int;
*p = 4;
delete p;
delete p;  // 不能多次 delete
delete [] 指针;  // 该指针必须指向 new 出来的空间

int* p = new int[20];
p[0] = 9;
delete [] p;


2 引用

作用: 给变量起别名

语法: 数据类型& 别名 = 原名​​​

  • 指针和引用的补充或者注意事项:
int a = 10;
// 等价 int* const b = &a;这里也说明了为啥引用一旦初始化之后,就不能再改变了
int& b = a; 

a = 100;  // 此时,a 和 b 的值都是 100
b = 200;  // 此时,a 和 b 的值都是 200


int x = 4;
const int& y = x;  // 注意这里的 const 修饰的 y,而不是 x !!!

x = 9;  // 这里仍然可以修改 x 的值,x 和 y 的值都是 9
// y = 99;  // 错误!!因为是 const 修饰的 y

----------------------------------------

// 指针复习
int n = 4;

int* ptr = &n;
*ptr = 10000;

// 限定 p,既不能修改 p 的指向,也不能修改 p 指向的值,这里说指向的值,是不能用 *p 修改 n 的值
const int* const p = &n;
n = 10;
// *ptr = 9;  // 错误!!表达式必须是可以修改的左值

特点:

  • 引用必须初始化

  • 引用在初始化后,不可以改变(因为引用的本质其实是伪指针)

    int rat;
    int& rodent = rat;  // rodent 和 *ptr 是等价的
    int* const ptr = &rat;  // 指针常量,不能改变指针的指向,但是可以改变指向的值
    
  • 当形参为 const 引用与实参不匹配时,c++ 将生成临时变量,则其行为类似于按值传递,为了确保原始数据不被修改,将使用临时变量存值,以下两种情况会生成临时变量:
    1.实参类型正确,但不是左值(可被引用的数据对象,即可通过地址访问的对象)
    2.实参类型不正确,但可以转为正确的类型

  • 如果声明将引用指定为 const,c++ 将在必要的时候生成临时变量,但是 const 修饰的引用变量,之后是不可以更改的

double refcube(const double& a)
{
    
    
 	return a*a*a;
}

double side = 3.0;
long edge = 5L;
double lens[4] = {
    
    2.0, 5.0, 10.0, 12.0};

double c1 = refcube(side);  // a 是 side
double c2 = refcube(lens[2]);  // a 是 lens[2]
double c3 = refcube(edge);  // 虽然 c3 的结果是 125,但是 a 是临时变量
  • 现在,如果是普通的引用修饰形参的话,实参与形参类型不匹配,是会报错的,不像之前,会有一个临时变量的转换过程
  • 这里补充一下知识点:非左值包括字面常量(字符串常量除外,他们有自己的地址)和包含多项的表达式
  • 函数传参时,引用可以让形参修饰实参,可以简化指针修改实参
  • 引用必须是要在栈或堆上的变量,不能直接引用常量,如 int& a = 10;​​ 这里说的不能直接引用常量是因为,常量在内存中,是临时地址,随时都能被释放

总结:通过引用参数产生的效果同按地址传递是一样的,引用的语法更简洁


  • 引用是可以作为函数的返回值

注意:不要返回局部变量引用

用法:函数调用作为左值

#include<iostream>
#include<string>

using namespace std;

// 返回局部变量引用
int& test01() {
    
    
	int a = 10;  // 局部变量
	return a;
}

// 返回静态变量引用
int& test02() {
    
    
	static int a = 20;
	return a;
}

int main() {
    
    
	// 不能返回局部变量的引用
	int& ref = test01();
	cout << "ref = " << ref << endl;  // ref = 10
	//cout << "ref = " << ref << endl;  // test01 返回的是局部变量,函数调用完之后,a 的内存被释放了,因此 ref 也被释放

	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl;  // ref2 = 20
	cout << "ref2 = " << ref2 << endl;  // ref2 = 20
  
	// 如果函数做左值,那么必须返回引用
	test02() = 1000;
	cout << "ref2 = " << ref2 << endl;  // ref2 = 1000
	cout << "ref2 = " << ref2 << endl;  // ref2 = 1000
   
	return 0;
}

引用的本质在 c++ 内部实现是一个指针常量,但是所有的指针操作编译器都帮我们做了

// 发现是引用,转换为 int* const ref = &a;
void func(int& ref)
{
    
    
	ref = 100;  // ref 是引用,转换为 *ref = 100
}
int main()
{
    
    
	int a = 10;
    
   	// 自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
	int& ref = a; 
	ref = 20;  // 内部发现 ref 是引用,自动帮我们转换为: *ref = 20;
    
	func(a);

	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;

	return 0;
}
  • 常量引用主要用来修饰形参,防止误操作
// 引用使用的场景,通常用来修饰形参
void showValue(const int& v)
{
    
    
	// v += 10;  // 因为是 const 修饰,所以不能修改 v 的值
	cout << v << endl;
}

int main()
{
    
    
	// int& ref = 10;  引用本身需要一个合法的内存空间,因此这行错误
	// 但是如果是 const 修饰的引用,编译器优化代码 int temp = 10; const int& ref = temp;
	const int& ref = 10;

	// ref = 100;  // 加入const后不可以修改变量
	cout << ref << endl;

	// 函数中利用常量引用防止误操作修改实参
	int a = 10;
	showValue(a);

	return 0;
}

补充:

  • 不能把常量指针赋值给非常量指针,反过来可以;不能把常量引用赋值非常量引用,反过来可以
int a = 10;
const int& x = a;
// int& y = x;  // 这里会报错,因为将 int& 类型的引用绑定到 const int& 类型的初始值设定时,限定符会被丢弃

-----------------------------------------------

int b = 10;
int& m = b;
const int& n = m;  // 这里是可以正常编译的

b = 20;  // b m n 的值都是 20
m = 49;  // b m n 的值都是 49
// n = 99;  // 错误!!表达式必须是可以修改的左值

-----------------------------------------------

// 指针同上,但是也可以通过强制类型转换的方式,把常量指针赋值给非常量指针
const int* p1;
int* p2;

p1 = p2;  // ok
p2 = p1;  // error

p2 = (int*) p1;  // ok,强制转换

常量成员函数补充:

  • 在类的成员函数声明后面加 const 关键字,则该成员函数是常量成员函数

  • 常量成员函数执行期间,不应该修改其作用的对象(不能修改成员变量的值,静态成员变量除外;也不能调用同类的非常量成员函数 [因为可能会更改],静态成员函数除外)

  • 两个成员函数,名字和参数一样,但是一个是 const,一个不是,算重载


3 函数

3.1 普通函数

  • 函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

  • 函数参数可缺省的目的在于提高程序的可扩充性

语法: 返回值类型 函数名 (数据类型){}​​

// 函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
    
    
	cout << "this is func" << endl;
}

int main()
{
    
    
	func(10, 10);  // 占位参数必须填补

	return 0;
}

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

函数重载满足条件:

  • 同一个作用域下
  • 函数名相同
  • 函数参数类型不同或者个数不同或者顺序不同

注意: 函数的返回值不可以作为函数重载的条件

函数重载注意事项:

  • 引用作为重载条件
  • 函数重载碰到函数默认参数
// 1、引用作为重载条件
void func(int& a)  // int& a = 10; 错误!
{
    
    
	cout << "func (int& a) 调用 " << endl;
}

// const int& a = 10; 注意这个是合法的!
// 相当于 int tmp = 10; const int& a = tmp;
void func(const int& a) 
{
    
    
	cout << "func (const int& a) 调用 " << endl;
}

// 2、函数重载碰到函数默认参数
void func2(int a, int b = 10)
{
    
    
	cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
    
    
	cout << "func2(int a) 调用" << endl;
}

int main()
{
    
    
	int a = 10;
	func(a);  // 调用无 const
	func(10);  // 调用有 const

	// func2(10);  // 碰到默认参数产生歧义,需要避免

	return 0;
}

3.2 内联函数

  • 函数调用是有时间开销的,如果函数本身只有几条语句,执行的非常快。相比之下,调用函数产生的开销就会大很多

  • 为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,时将整个函数的代码插入到调用语句处,而不会产生调用函数的语句

  • 在函数定义前加 inline 关键字,即可定义内联函数

inline int Max(int a, int b)
{
    
    
	if (a > b) return a;
	return b;
}

4 类和对象

结构化程序设计:程序 = 数据结构 + 算法

面向对象的程序 = 类 + 类 + … + 类

设计程序的过程,就是设计类的过程!

面向对象的程序设计方法:

  • 将某类客观事物共同特点(属性)归纳出来,形成一个数据结构
  • 将这类事物能进行的行为也归纳出来,形成一个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)

  • 类的成员函数和类的定义分开写(类外的定义需要使用 :: 运算符限定作用域)
class CRectangle
{
    
    
public:
	int w, h;
	int Area();  // 声明成员函数
	int Perimeter();
	void Init(int w_, int h_);
};

int CRectangle::Area()
{
    
    
	return w * h;
}

int CRectangle::Perimeter()
{
    
    
	return 2 * (w + h);
}

void CRectangle::Init(int w_, int h_)
{
    
    
	w = w_; h = h_;
}

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

C++ 认为万事万物皆对象,对象上有属性和行为


4.1 封装

  • 类中的变量称为成员变量,类中的函数称为成员函数,类定义出来的变量,也称为类的实例,也是“对象”

  • 每个对象各有自己的存储空间,一个对象的某个成员变量被改变了,不会影响到另一个对象

  • 通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个类,从而使得数据结构和操作该数据结构的算法呈现出紧密关系,这就是“封装”

封装的意义:

  • 将属性和行为作为一个整体,表现为事物
  • 将属性和行为加以权限控制

访问权限有三种:

  1. public 公共权限 类内可以访问 类外可以访问
  2. protected 保护权限 类内可以访问 类外不可以访问
  3. private 私有权限 类内可以访问 类外不可以访问

设置私有成员的机制,叫“隐藏”,隐藏的目的是强制对成员变量的访问一定要通过成员函数进行,以后需要对成员变量的类型等属性修改后,只修改成员函数即可。否则,所有直接访问成员变量的语句都需要修改

在类的成员函数内部,能访问:

  • 当前对象的全部属性、函数
  • 同类其他对象的全部属性、函数

在类的成员函数以外的地方,只能访问该类对象的公有成员

在 C++ 中 struct 和 class 唯一的区别就在于默认的访问权限不同

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

4.2 对象的初始化和清理

C++ 中的面向对象来源于生活,每个对象都有初始设置以及对象销毁前的清理数据


4.2.1 构造函数和析构函数

对象的初始化和清理是两个非常重要的安全问题

c++ 利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供

编译器提供的构造函数和析构函数是空实现

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

构造函数语法:类名(){}​​

  1. 构造函数,没有返回值也不写 void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次

析构函数语法: ~类名(){}​​

  1. 析构函数,没有返回值也不写 void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

析构函数的两种分类方式:

  • 按参数分为: 有参构造和无参构造(默认)
  • 按类型分为: 普通构造和拷贝构造

析构函数的三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
// 1、构造函数分类
class Person 
{
    
    
public:
	// 无参(默认)构造函数
	Person() {
    
    
		cout << "无参构造函数!" << endl;
	}

	// 有参构造函数
	Person(int a) {
    
    
		age = a;
		cout << "有参构造函数!" << endl;
	}

	// 拷贝构造函数
	Person(const Person& p) {
    
      // 限定拷贝的时候,不能更改实参
		age = p.age;
		cout << "拷贝构造函数!" << endl;
	}

	// 析构函数
	~Person() {
    
    
		cout << "析构函数!" << endl;
	}
public:
	int age;
};

// 2、构造函数的调用
void test01() {
    
    
	Person p;  // 调用无参构造函数
}

// 调用有参的构造函数
void test02() {
    
    

	// 2.1 括号法(常用)
	Person p1(10);

	// Person p2();  // 注意1:调用无参构造函数不能加括号,否则编译器认为这是函数声明

	// 2.2 显式法
	Person p2 = Person(10); 
	Person p3 = Person(p2);
	// Person(10);  // 单独写就是匿名对象,当前行结束之后,马上析构

	// 注意2:这里不要利用拷贝构造函数初始化匿名对象,编译器会认为是对象声明
	// Person(p9);  // Person(p9) == Person p9;

	// 2.3 隐式转换法
	Person p4 = 10;  // Person p4 = Person(10); 
	Person p5 = p4;  // Person p5 = Person(p4);  // 拷贝构造

	// Person p5(p4);
}

C++ 中拷贝构造函数调用时机通常有三种情况:

  • 用一个对象去初始化同类的另一个对象
  • 如果某函数有一个参数是类 A 的对象,那么该函数被调用时,类 A 的拷贝构造函数将被调用
  • 如果函数的返回值是类 A 的对象,则函数返回时,A 的拷贝改造函数被调用
class Person
{
    
    
public:
	Person() {
    
    
		cout << "无参构造函数!" << endl;
		mAge = 0;
	}
	Person(int age) {
    
    
		cout << "有参构造函数!" << endl;
		mAge = age;
	}
	Person(const Person& p) {
    
    
		cout << "拷贝构造函数!" << endl;
		mAge = p.mAge;
	}

	~Person() {
    
    
		cout << "析构函数!" << endl;
	}
public:
	int mAge;
};

// 1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
    
    
	Person man(100);  // p 对象已经创建完毕
	Person newman(man);  // 调用拷贝构造函数
	Person newman2 = man;  // 拷贝构造
	// 隐式转换法
	// Person newman3 = man;  // 不是调用拷贝构造函数
}

// 2. 值传递的方式给函数参数传值
// 相当于Person p1 = p;
void doWork(Person p1) {
    
    }
void test02()
{
    
    
	Person p;  // 无参构造函数
	doWork(p);
}

// 3. 以值方式返回局部对象
Person doWork2()
{
    
    
	Person p1;  // 局部对象在 doWork2() 作用完后,随即被释放
	cout << (int*)&p1 << endl;

	// 注意这里返回 p1 是可以的,因为他在这里创建了一个副本,这个副本指的是拷贝构造函数产生的对象,生命周期要看它被使用到什么时候!只有返回引用和指针类型的数据才会报错
	return p1;  // 这里返回的不是 p1 本身,而是创建一个 p1 副本返回
}

void test03()
{
    
    
	Person p = doWork2();
	cout << (int*)&p << endl;
}

默认情况下,编译器至少给一个类添加 3 个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则:

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

注意:构造函数知识点补充

  1. 如果自定义了有参构造函数,那么就不能再使用无参构造函数定义变量
class Complex
{
    
    
private:
	double real, imag;
public:
	Complex(double r, double i = 0);
};

Complex::Complex(double r, double i)
{
    
    
	real = r, imag = i;
}

int main()
{
    
    
	Complex c1;  // error,缺少构造函数的参数
	Complex* pc = new Complex;  // error,缺少构造函数的参数
	Complex c1(2);  // OK
	Complex* pc = new Complex;  // OK

	return 0;
}

  1. 构造函数在数组中的使用
class CSample
{
    
    
	int x;
public:
	CSample()
	{
    
    
		cout << "Constructor 1 Called" << endl;
	}
	CSample()
	{
    
    
		x = n;
		cout << "Constructor 2 Called" << endl;
	}
};

int main()
{
    
    
	CSample array[2];  // 输出两次 Constructor 1 Called
	cout << "step1" << endl;

	CSample array2[2] = {
    
     4, 5 };
	cout << "step2" << endl;  // 输出两次 Constructor 2 Called

	CSample array3[2] = {
    
    3};
	cout << "step3" << endl;  // Constructor 2 Called,Constructor 1 Called

	CSample* array4 = new CSample[2];  // 输出两次 Constructor 1 Called

	delete[] array4;

	return 0;
}


补充:

01 拷贝构造函数:

  • 只有一个参数,即对同类对象的引用
  • 形如 X::X( X& )或 X::X( const X& ),二者选一,注意参数必须是引用!后者能以常量对象作为参数
  • 如果没有定义拷贝构造函数,那么编译器生成默认复制构造函数,默认的拷贝构造函数完成复制功能

注意:

  • 对象间赋值并不导致拷贝构造函数被调用
class CMyclass
{
    
    
public:
	int n;
	CMyclass() {
    
    };
	CMyclass(CMyclass& c) {
    
    
		n = 2 * c.n;  // 拷贝构造函数一般不这么做,这里只是为了举例子
	};
};

int main()
{
    
    
	CMyclass c1, c2;
	c1.n = 5;
	c2 = c1;  // 赋值语句,不是初始化语句,因此这里不会调用拷贝构造函数
	CMyclass c3(c1);  // 会调用拷贝构造函数

	cout << "c2.n=" << c2.n << ",";  // 5
	cout << "c3.n=" << c3.n << endl;  // 10

	return 0;
}

02 常量引用参数的使用

void fun(CMyclass obj_)
{
    
    
	cout << "fun" << endl;
}
  • 上面的函数,调用时生成形参会引发拷贝构造函数调用,开销大。因此,考虑使用 CMyclass& 引用类型作为参数
  • 如果希望实参的值在函数中不被改变,可以加上 const 关键字

03 类型转换构造函数

  • 定义转换构造函数的目的是实现类型的自动转换
  • 只有一个参数,而且不是拷贝构造函数的构造函数,一般就可以看作是转换构造函数
  • 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)
class Complex
{
    
    
public:
	double real, imag;
	Complex(int i)  // 类型转换构造函数,如果碰到类型不匹配的,那么会生成临时变量来接收
	{
    
    
		cout << "IntConstructor called" << endl;
		real = i;
		imag = 0;
	}

	Complex(double r, double i) {
    
     real = r; imag = i; }
};

int main()
{
    
    
	Complex c1(7, 8);
	Complex c2 = 12;
	// 因为这里直接赋值的话,类型是不匹配的。9 被自动转为临时 Complex 对象
	c1 = 9;

	cout << c1.real << "," << c1.imag << endl;

	return 0;
}

04 析构函数和数组

  • 对象数组声明周期结束时,对象数组的每个元素的析构函数都会被调用
class Ctest
{
    
    
public:
	~Ctest() {
    
     cout << "destructor called" << endl; }
};

int main()
{
    
    
	Ctest array[2];  // 输出两次 destructor called
	cout << "End main" << endl;

	return 0;
}

05 析构函数和运算符 delete

  • delete 运算导致析构函数调用
  • 如果 new 一个对象数组,用 delete 释放时应该写 [],否则只 delete 一个对象(调用一次析构函数)
Ctest* pTest;
pTest = new Ctest;  // 构造函数调用
delete pTest;  // 析构函数调用
------------------------------------------
pTest = new Ctest[3];  // 调用 3 次构造函数
delete[] pTest;  // 调用 3 次析构函数

06 析构函数在对象作为函数返回值后被调用

class CMyclass
{
    
    
public:
	~CMyclass() {
    
     cout << "destructor" << endl; }
};

CMyclass obj;  // 全局函数变量,整个程序结束时,也会调用析构函数

// 这里的形参被传入参数时,调用 CMyclass 的构造函数,当参数对象消亡,也会调用析构函数
CMyclass fun(CMyclass sobj)
{
    
    
	// 这里注意 2 个点
	// 1.这里生成返回的临时对象
	return sobj;  // 函数调用返回时,返回的是生成的临时对象
}

int main()
{
    
    
	// 函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用
	// 这里是用临时对象给 obj 赋值
	// 2.临时对象的生命周期在执行完下行语句后消亡
	obj = fun(obj);  // 输出 3 次 destructor

	return 0;
}

07 构造函数和析构函数的调用时机

class CMyclass
{
    
    
	int id;
public:
	CMyclass(int i)  // 类型转换构造函数
	{
    
    
		id = i;
		cout << "id = " << id << " constructed" << endl;
	}
	~CMyclass() {
    
     cout << "id = " << id << " destructed" << endl; }
};

CMyclass d1(1);  // 因为这里有全局变量,所以 d1 调用构造函数比 main 中的还早

void Func()
{
    
    
	static CMyclass d2(2);
	CMyclass d3(3); 

	cout << "func" << endl;
}

int main()
{
    
    

	/* 程序执行输出结果
	id = 1 constructed
	id = 4 constructed
	id = 6 constructed
	id = 6 destructed
	main
	id = 5 constructed
	id = 5 destructed
	id = 2 constructed
	id = 3 constructed
	func
	id = 3 destructed
	main ends!
	id = 6 destructed
	id = 2 destructed
	id = 1 destructed
	*/

	CMyclass d4(4);
	d4 = 6;
	cout << "main" << endl;

	{
    
    CMyclass d5(5); }  // 局部变量,遇到有大括号后,变量使用结束

	Func();

	cout << "main ends!" << endl;

	return 0;
}

4.2.2 深拷贝与浅拷贝

浅拷贝:引用做函数返回值,会带来堆区的内存重复释放(一般是在调用构造函数有时产生的问题)

深拷贝:在堆区重新申请空间,进行拷贝操作

class Person
{
    
    
public:
	// 无参(默认)构造函数
	Person() {
    
    
		cout << "无参构造函数!" << endl;
	}

	// 有参构造函数
	Person(int age, int height) {
    
    
		cout << "有参构造函数!" << endl;

		m_age = age;
		m_height = new int(height);
	}

	// 自定义拷贝构造函数,解决浅拷贝带来的问题
	Person(const Person& p) {
    
    
		cout << "Person 拷贝构造函数!" << endl;

		m_age = p.m_age;
		// m_Height = p.m_height;  // 编译器默认写的浅拷贝

		// 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		m_height = new int(*p.m_height);  // 这里要加括号,是固定用法,记住就行
	}

	// 析构函数
	~Person() {
    
    
		cout << "析构函数!" << endl;
		if (m_height != NULL)
		{
    
    
			delete m_height;
			m_height = NULL;
		}
	}
public:
	int m_age;
	int* m_height;  // 指针变量存放在栈,指针指向的值存放在堆中
};

void test01()
{
    
    
	Person p1(18, 180);  // 因为 m_height 是指针变量,所以,*m_height 的值被存放在堆中

	// 因为 p1 和 p2 是在栈中存储的,所以存储的时候,是先存储 p1 再存储 p2
	// 因此在释放(调用析构函数)的时候,先释放 p2 再释放 p1
	Person p2(p1);  // 拷贝构造函数,是浅拷贝。所以,p1 和 p2 的 m_height 指向的是同一块内存空间

	// 注意:这里用指针,是为了更好地突出深拷贝和浅拷贝面临的问题
	cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
	cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题


4.2.3 初始化列表

作用:用来初始化属性

语法:构造函数():属性1(值1),属性2(值2)... {}​​​

成员对象和封闭类:有成员对象的类叫封闭(enclosing)类

class Person
{
    
    
public:
	// 初始化列表方式初始化
	Person(int a, int b, int c): m_A(a), m_B(b), m_C(c) {
    
    }

	void PrintPerson()
	{
    
    
		cout << "mA:" << m_A << endl;
		cout << "mB:" << m_B << endl;
		cout << "mC:" << m_C << endl;
	}

private:
	int m_A;
	int m_B;
	int m_C;
};

int main() 
{
    
    
	Person p(1, 2, 3);
	p.PrintPerson();

	return 0;
}

4.2.4 类对象作为类成员

对象成员:类中的成员是另一个类的对象。B 类中有对象 A 作为成员,A 为对象成员

class A {
    
    }
class B
{
    
    
	A a;
}

当创建 B 对象时,构造的顺序是:先调用对象成员(A)的构造函数,再调用本类构造;析构顺序相反

封闭类的复制构造函数

class A
{
    
    
public:
	A() {
    
     cout << "default" << endl; }
	A(A &a) {
    
     cout << "copy" << endl; }
};

class B {
    
     A a; };  // 既有无参构造函数,也有拷贝构造函数

int main()
{
    
    
	/*输出
	default
	copy*/

	// 说明 b2.a 是用类 A 的拷贝构造函数初始化的。而且调用拷贝构造函数时的实参就是 b1.a
	B b1;  // 这里输出的是 default
	B b2(b1);  // 此时,拷贝构造函数不是简单地把 b1 拷贝给 b2,是用类 A 的拷贝构造函数初始化的

	return 0;
}

4.2.5 静态成员

  • 静态成员就是在成员变量和成员函数前加上关键字 static,称为静态成员

  • 普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享
  • sizeof 不会计算静态成员变量
  • 普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象;因此,静态成员不需要通过对象就能访问

// 1.类名::成员名
CRectangle::PrintTotal();

// 2.对象名.成员名
CRectangle r;
r.PrintTotal();  // 这里要注意,PrintTotal 并不是作用在 r 上的

// 3.指针->成员名
CRectangle* p = &r;
p->PrintTotal();  // 这里要注意,PrintTotal 并不是作用在 p 上的

// 4.引用.成员名
CRectangle& ref = r;
int n = ref.nTotalNumber;  // 这里要注意,nTotalNumber并不是作用在 ref 上的

  • 静态成员变量本质上是全局变量,即使一个对象都不存在,类的静态成员变量也存在
  • 静态成员函数本质上是全局函数

静态成员分为:

  • 静态成员变量

    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • ​​静态成员函数

    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

设置静态成员的机制的目的是将和某些紧密相关的全局变量和函数写到类里,看上去像一个整体,易于维护和理解

必须在定义类的文件中,对静态成员变量进行一次说明或初始化,否则编译能通过,链接不能通过


4.3 c++对象模型和 this 指针

在 C++ 中,类内的成员变量和成员函数分开存储

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

class Person
{
    
    
public:
	Person()
	{
    
    
		mA = 0;
	}

	int mA;  // 非静态成员变量占对象空间
	static int mB;  // 静态成员变量不占对象空间

	// 普通方法也不占对象空间,所有方法共享一个函数实例
	void func() {
    
    
		cout << "mA:" << this->mA << endl;
	}

	// 静态成员函数也不占对象空间
	static void sfunc() {
    
    
	}
};

int main()
{
    
    
	cout << sizeof(Person) << endl;  // 4

	return 0;
}
  • this 指针指向被调用的成员函数所属的对象

  • this 指针是隐含每一个非静态成员函数内的一种指针

  • this 指针不需要定义,直接使用即可

this 指针的用途:

  • 当形参和成员变量(类内的变量)同名时,可用 this 指针来区分
  • 在类的非静态成员函数中返回对象本身****(类外的对象),可使用 return *this
class Person
{
    
    
public:
	Person(int age)
	{
    
    
		// 1、当形参和成员变量同名时,可用 this 指针来区分
		this->age = age;  // this 指向的是类内非静态成员变量
	}

	Person& PersonAddPerson(Person p)
	{
    
    
		this->age += p.age;

		return *this;  // 返回对象本身,后续可以进行链式调用
	}

	int age;
};

void test01()
{
    
    
	Person p1(10);
	cout << "p1.age = " << p1.age << endl;  // p1.age = 10

	Person p2(10);
	p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);  // 链式调用必须得是对象
	cout << "p2.age = " << p2.age << endl;  // p2.age = 40
}

C++ 中空指针也是可以调用成员函数的,但是也要注意有没有用到 this 指针

如果用到 this 指针,需要加以判断保证代码的健壮性

// 空指针访问成员函数
class Person {
    
    
public:

	void ShowClassName()
	{
    
    
		cout << "我是Person类!" << endl;
	}

	void ShowPerson()
	{
    
    
		if (this == NULL) {
    
    
			return;
		}

	        // 因为 p 是空指针,没有一个确切的对象,更无法访问它的值了
		cout << mAge << endl;  // 这里其实有隐藏 buff,mAge 其实是 this->mAge
	}

public:
	int mAge;
};

void test01()
{
    
    
	Person* p = NULL;
	p->ShowClassName();  // 空指针,可以调用成员函数
	// p->ShowPerson();  // 但是如果成员函数中用到了 this 指针,就不可以了
}

4.4 友元 friend

友元分类友元函数和友元类两种

友元:在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问

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

友元之间的关系不能传递,也不能继承

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

只介绍成员函数做友元,其他类似

class Building;
class goodGay
{
    
    
public:
   	// 类中声明,类外定义,定义需要使用 :: 运算符
	goodGay();
	void visit();  // 只让 visit 函数作为 Building 的好朋友,可以发访问 Building 中私有内容
	void visit2();   // 不让 vist2 访问私有成员
private:
	Building* building;
};

class Building
{
    
    
	// 告诉编译器 goodGay 类中的 visit 成员函数是 Building 好朋友,可以访问私有内容
	friend void goodGay::visit();  // 注意哦:这里要声明作用域,因为 visit 是全局函数,否则会出错

public:
	Building();

public:
	string m_SittingRoom;  // 客厅

private:
	string m_BedRoom;  // 卧室
};

// 构造函数的定义(这里是为了锻炼,写在类内也可以的)
// a::b -- a 是限定作用域,意思就是 a 的 b
Building::Building()
{
    
    
	this->m_SittingRoom = "客厅";
	this->m_BedRoom = "卧室";
}

goodGay::goodGay()
{
    
    
	building = new Building;
}

void goodGay::visit()
{
    
    
	cout << "好基友正在访问" << building->m_SittingRoom << endl;
	cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void goodGay::visit2()
{
    
    
	cout << "好基友正在访问" << building->m_SittingRoom << endl;
	// cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
    
    
	goodGay  gg;
	gg.visit();

}

4.5 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

运算符重载的实质是函数重载,可以重载为普通函数,也可以重载为成员函数


4.5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算

class Person
{
    
    
public:
	Person() {
    
    };
	Person(int a, int b)
	{
    
    
		this->m_A = a;
		this->m_B = b;
	}

	// 成员函数实现 + 号运算符重载
	Person operator+(const Person& p) {
    
    
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}
public:
	int m_A;
	int m_B;
};

// 全局函数实现 + 号运算符重载
// Person operator+(const Person& p1, const Person& p2) {
    
    
//	Person temp(0, 0);
//	temp.m_A = p1.m_A + p2.m_A;
//	temp.m_B = p1.m_B + p2.m_B;
//	return temp;
//}

// 运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)  
{
    
    
	Person temp;
	temp.m_A = p2.m_A + val;
	temp.m_B = p2.m_B + val;
	return temp;
}

void test() {
    
    
	Person p1(10, 10);
	Person p2(20, 20);

	// 成员函数方式
	Person p3 = p2 + p1;  // 相当于 p2.operaor+(p1)
	cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;

	Person p4 = p3 + 10;  // 相当于 operator+(p3,10)
	cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}

总结:

  • 对于内置的数据类型的表达式的的运算符是不可能改变的

  • 不要滥用运算符重载


4.5.2 左移运算符重载

作用:可以输出自定义数据类型

class Person
{
    
    
	friend ostream& operator<<(ostream& out, Person& p);
public:
	Person(int a, int b)
	{
    
    
		this->m_A = a;
		this->m_B = b;
	}
	// 成员函数 实现不了  p << cout 不是我们想要的效果
	//void operator<<(Person& p){
    
    
	//}
private:
	int m_A;
	int m_B;
};

// 全局函数实现左移重载
// ostream 对象只能有一个
ostream& operator<<(ostream& out, Person& p)
{
    
    
	out << "a:" << p.m_A << " b:" << p.m_B;
	return out;
}

void test() 
{
    
    
	Person p1(10, 20);
	cout << p1 << "hello world" << endl;  // 链式编程
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型


4.5.3 递增运算符重载

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

class MyInteger
{
    
    
	friend ostream& operator<<(ostream& out, MyInteger myint);
public:
	MyInteger() {
    
    
		m_Num = 0;
	}
	// 前置++;注意这里的返回值类型必须要有 &,否则多次使 用 ++,不起效果(只有第一次会实现 ++),因为在第一次使用完这个值之后就会释放掉了
    	// 返回引用是为了一直对一个数进行递增
	MyInteger& operator++() {
    
    
		m_Num++;  // 先++

		return *this;  // 再返回
	}

	// 后置++;这里其实是前置++函数的重载
	MyInteger operator++(int) {
    
      // 注意这里需要有一个占位符,用来区分前置、后置,因为函数返回类型不作为重载的条件
		// 先返回
		MyInteger temp = *this;  // 记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后 ++
		m_Num++;
		return temp;  // 后置 ++,一定要返回的是值,因为这里用到了局部变量 temp
	}
private:
	int m_Num;
};

ostream& operator<<(ostream& out, MyInteger myint) {
    
    
	out << myint.m_Num;
	return out;
}

// 前置++ 先++ 再返回
void test01() {
    
    
	MyInteger myInt;
	cout << ++myInt << endl;
	cout << myInt << endl;
}

// 后置++ 先返回 再++
void test02() {
    
    
	MyInteger myInt;
	cout << myInt++ << endl;
	cout << myInt << endl;
}

总结: 前置递增返回引用,后置递增返回值


4.5.4 赋值运算符重载

  • 赋值运算符,只能重载为成员函数

c++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

class Person
{
    
    
public:
	Person(int age)
	{
    
    
		// 将年龄数据开辟到堆区
		m_Age = new int(age);
	}

	// 重载赋值运算符
	Person& operator=(Person &p)
	{
    
    
		if (m_Age != NULL)
		{
    
    
			delete m_Age;
			m_Age = NULL;
		}
		// 编译器提供的代码是浅拷贝
		// m_Age = p.m_Age;

		// 提供深拷贝,解决浅拷贝的问题
		m_Age = new int(*p.m_Age);

		// 返回自身
		return *this;
	}

	~Person()
	{
    
    
		if (m_Age != NULL)
		{
    
    
			delete m_Age;
			m_Age = NULL;
		}
	}
	// 年龄的指针
	int *m_Age;
};

void test01()
{
    
    
	Person p1(18);
	Person p2(20);
	Person p3(30);

	p3 = p2 = p1;  // 赋值操作

	cout << "p1的年龄为:" << *p1.m_Age << endl;  // 18
	cout << "p2的年龄为:" << *p2.m_Age << endl;  // 18
	cout << "p3的年龄为:" << *p3.m_Age << endl;  // 18
}

int main()
{
    
    
	test01();

	int a = 10;
	int b = 20;
	int c = 30;

	c = b = a;
	cout << "a = " << a << endl;  // 10
	cout << "b = " << b << endl;  // 10
	cout << "c = " << c << endl;  // 10

	return 0;
}

4.5.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

class Person
{
    
    
public:
	Person(string name, int age)
	{
    
    
		this->m_Name = name;
		this->m_Age = age;
	};

	bool operator==(Person& p)
	{
    
    
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
		{
    
    
			return true;
		}
		else
		{
    
    
			return false;
		}
	}

	bool operator!=(Person& p)
	{
    
    
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
		{
    
    
			return false;
		}
		else
		{
    
    
			return true;
		}
	}

	string m_Name;
	int m_Age;
};

void test01()
{
    
    
	//int a = 0;
	//int b = 0;

	Person a("孙悟空", 18);
	Person b("孙悟空", 18);

	if (a == b)
	{
    
    
		cout << "a和b相等" << endl;
	}
	else
	{
    
    
		cout << "a和b不相等" << endl;
	}

	if (a != b)
	{
    
    
		cout << "a和b不相等" << endl;
	}
	else
	{
    
    
		cout << "a和b相等" << endl;
	}
}

4.5.6 函数调用运算符重载

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
class MyPrint
{
    
    
public:
	void operator()(string text)
	{
    
    
		cout << text << endl;
	}
};

void test01()
{
    
    
	// 重载的()操作符,也称为仿函数
	MyPrint myFunc;
	myFunc("hello world");
}

class MyAdd
{
    
    
public:
	int operator()(int v1, int v2)
	{
    
    
		return v1 + v2;
	}
};

void test02()
{
    
    
	MyAdd add;
	int ret = add(10, 10);
	cout << "ret = " << ret << endl;

	// 匿名对象调用  
	cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

4.6 继承

下级别的成员除了拥有上一级的共性,还有自己的特性。可以利用继承,减少重复代码

继承的语法:class 子类 : 继承方式 父类​​​

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承
class Base1
{
    
    
public: 
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

// 公共继承
class Son1: public Base1
{
    
    
public:
	void func()
	{
    
    
		m_A;  // 可访问 public 权限
		m_B;  // 可访问 protected 权限
		// m_C;  // 不可访问
	}
};

void myClass()
{
    
    
	Son1 s1;
	s1.m_A;  // 其他类只能访问到公共权限
}

// 保护继承
class Base2
{
    
    
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son2: protected Base2
{
    
    
public:
	void func()
	{
    
    
		m_A;  // 可访问 protected 权限
		m_B;  // 可访问 protected 权限
		// m_C; // 不可访问
	}
};

void myClass2()
{
    
    
	Son2 s;
	// s.m_A;  // 不可访问
}

// 私有继承
class Base3
{
    
    
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son3: private Base3
{
    
    
public:
	void func()
	{
    
    
		m_A;  // 父类公共权限可访问,但是变量权限改为 private 权限
		m_B;  // 父类保护权限可访问,但是变量权限改为 private 权限
		//m_C;  // 父类私有权限不可访问
	}
};

class GrandSon3: public Son3
{
    
    
public:
	void func()
	{
    
    
		// Son3 是私有继承,所以继承 Son3 的属性在 GrandSon3 中都无法访问到
		//m_A;
		//m_B;
		//m_C;
	}
};

结论: 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到


  • 子类继承父类后,当创建子类对象,也会调用父类的构造函数
  • 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

在派生类对象中,包含及基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前

  • 继承:“是”的关系

    • 基类 A,B 是派生类
    • 逻辑要求:一个 B 对象也是一个 A 对象

  • 符合:“有”的关系

    • 类 C 中“有”成员变量 k,k 是类 D 的对象,则 C 和 D 是复合关系
    • 一般逻辑要求:D 对象是 C 对象的固有属性或组成部分

当子类与父类出现同名的成员:

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域,父类名::成员名

创建派生类的对象时,
1)先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;

2)在执行成员对象类的构造函数,用以初始化派生类对象中成员对象

3)最后执行派生类自己的构造函数

调用基类构造函数的两种方式:

- 显式方式:在派生类的构造函数中,为基类的构造函数提供参数  
- 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数自动调用基类的默认构造函数

public 继承的赋值兼容规则:

class base{
    
    };
class derived: public base{
    
    };
base b;
derived d;

1)派生类的对象可以赋值给基类对象(一个派生类对象是一个基类对象)
b = d; // 把 d 内容拷贝给 b

2)派生类对象可以初始化基类引用

base& br = d;

3)派生类对象的地址可以赋值给基类指针

base* pb = &d;

C++ 允许一个类继承多个类

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Og55A1P9-1685438574381)(assets/image-20230527124733-qjm1cxw.png)]

两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题
class Animal
{
    
    
public:
	int m_Age;
};

// 继承前加 virtual 关键字后,变为虚继承
// 此时公共的父类 Animal 称为虚基类
class Sheep : virtual public Animal {
    
    };
// 直接基类 Tuo
class Tuo   : virtual public Animal {
    
    };
// 派生类 SheepTuo,可以不用加 virtual
class SheepTuo : public Sheep, public Tuo {
    
    };

void test01()
{
    
    
	SheepTuo st;
	st.Sheep::m_Age = 100;  // 表示将 st.m_Age 赋值为 100,但是呢,m_Age 是在 Sheep 作用域下的
	st.Tuo::m_Age = 200;

	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;  // 200
	cout << "st.Tuo::m_Age = " <<  st.Tuo::m_Age << endl;  // 200
	cout << "st.m_Age = " << st.m_Age << endl;  // 200
}

4.7 多态

多态是 C++ 面向对象三大特性之一

多态分为两类

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

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

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

多态满足条件:

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件:

  • 父类指针或引用指向子类对象

“多态”的关键在于通过基类指针或引用调用虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 —— “动态联编”

重写:函数返回值类型 、函数名、参数列表完全一致称为重写

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

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

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

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

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
    
    
public:
	// 纯虚函数
	// 类中只要有一个纯虚函数就称为抽象类
	// 抽象类无法实例化对象
	// 子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};

class Son: public Base
{
    
    
public:
	virtual void func() 
	{
    
    
		cout << "func调用" << endl;
	};
};

void test01()
{
    
    
	Base* base = NULL;
	// base = new Base;  // 错误,抽象类无法实例化对象
	base = new Son;
	base->func();
	delete base;  // 记得销毁
}

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

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

  • 在类的定义中,有 virtual 关键字的成员函数就是虚函数
  • virtual 关键字只用在类定义里的函数声明中,写函数体时不用
  • 构造函数和静态成员函数不能是虚函数

看例子理解多态的实现原理:

class Base
{
    
    
public:
	int i;
	virtual void Print() {
    
     cout << "Base:Print"; }
};

class Derived : public Base
{
    
    
public:
	int n;
	virtual void Print() {
    
     cout << "Derived:Print"; }
};

int main()
{
    
    
	Derived d;
	cout << sizeof(Base) << ", " << sizeof(Derived);  // 8, 12

	return 0;
}

根据上述代码可以看出,每个输出都多了 4 个字节,从而引出多态实现的关键 —— 虚函数表

每一个有虚函数的类都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址,多出来的 4 个字节就是用来放虚函数表的地址的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BldKicUh-1685438574382)(assets/image-20230530155046-huvsjqw.png)]


通过基类指针删除派生类对象时,通常情况下,只能调用基类的析构函数,但是,删除一个派生类对象,应该先调用派生类的析构函数,然后调用基类的析构函数

class son
{
    
    
public:
	~son() {
    
     cout << "bye from son" << endl; }
};

class grandson :public son
{
    
    
	~grandson() {
    
     cout << "bye from grandson" << endl; }

};

int main()
{
    
    
	son* pson;
	pson = new grandson();
	delete pson;  // 输出 bye from son

	return 0;
}

解决办法:把基类的析构函数声明为 virtual
派生类的析构函数可以 virtual 不进行声明
通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

class son
{
    
    
public:
	virtual ~son() {
    
     cout << "bye from son" << endl; }
};

class grandson :public son
{
    
    
	~grandson() {
    
     cout << "bye from grandson" << endl; }
};

int main()
{
    
    
	son* pson;
	pson = new grandson();
	// 先输出 bye from grandson,在输出 bye from son
	delete pson;

	return 0;
}

一般来说,一个类如果定义了虚函数,应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义为虚函数

注意:不允许以虚函数作为构造函数


虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}​​

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}​​

  • 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内不能调用纯虚函数

总结:

1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类

5 文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++ 中对文件操作需要包含头文件

文件类型分为两种:

  1. 文本文件 - 文件以文本的 ASCII ​码形式存储在计算机中
  2. 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  1. ofstream:写操作(往文件输出)
  2. ifstream: 读操作(从文件输入)
  3. fstream : 读写操作

5.1文本文件

01 写文件步骤如下:

  1. 包含头文件
    #include <fstream>​​
  2. 创建流对象
    ofstream ofs;​​
  3. 打开文件
    ofs.open("文件路径", 打开方式);​​
  4. 写数据
    ofs << "写入的数据";​​
  5. 关闭文件
    ofs.close();​​

文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意: 文件打开方式可以配合使用,利用 | 操作符

例如:用二进制方式写文件 ios::binary | ios:: out​​

#include <fstream>

void test01()
{
    
    
	ofstream ofs;
	ofs.open("test.txt", ios::out);

	ofs << "姓名:张三" << endl;
	ofs << "性别:男" << endl;
	ofs << "年龄:18" << endl;

	ofs.close();
}

02 读文件步骤如下:

  1. 包含头文件
    #include <fstream>​​
  2. 创建流对象
    ifstream ifs;​​
  3. 打开文件并判断文件是否打开成功
    ifs.open("文件路径", 打开方式);​​
  4. 读数据
    四种方式读取​​
  5. 关闭文件
    ifs.close();​​
#include <fstream>
#include <string>

void test01()
{
    
    
	ifstream ifs;
	ifs.open("test.txt", ios::in);

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

	// 第一种方式
	//char buf[1024] = { 0 };
	//while (ifs >> buf)  // 按行读取
	//{
    
    
	//	cout << buf << endl;
	//}

	// 第二种
	//char buf[1024] = { 0 };
	//while (ifs.getline(buf, sizeof(buf)))  // 按行读取
	//{
    
    
	//	cout << buf << endl;
	//}

	// 第三种
	//string buf;
	//while (getline(ifs, buf))  // ifs -- 文件输入流对象,buf -- 文件读取之后存储的地方
	//{
    
    
	//	cout << buf << endl;
	//}

    	// 第四种,不推荐,使用前三种最好
	char c;
	while ((c = ifs.get()) != EOF)  // get() -- 按字符读取,EOF -- 文件尾部
	{
    
    
		cout << c;
	}

	ifs.close();
}

03 文件的读写指针

ofstream fout("a1.out", ios::app);  // 以添加的方式打开
long location = fout.tellp();  // 获取写指针的位置
location = 10;
fout.seekp(location);  // 将写指针移动到第 10 个字节处
fout.seekp(location, ios::beg);  // 从头数 location
fout.seekp(location, ios::cur);  // 从当前数 location
fout.seekp(location, ios::end);  // 从尾部数 location

5.2 二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为 ios::binary

01 写文件主要利用流对象调用成员函数 write

函数原型 :ostream& write(const char* buffer, long len);

参数解释:字符指针 buffer 指向内存中一段存储空间,len 是读写的字节数

#include <fstream>
#include <string>

class Person
{
    
    
public:
	char m_Name[64];
	int m_Age;
};

void test01()
{
    
    
	// 1、包含头文件
	// 2、创建输出流对象
	ofstream ofs("person.txt", ios::out | ios::binary);
		  
	// 3、打开文件
	//ofs.open("person.txt", ios::out | ios::binary);
	
	Person p = {
    
    "张三", 18};  // p 存储在栈中,而{"张三", 18}存储在堆中
	
	// 4、写文件
	// write(const char* _Str, streamsize_Count),所以这里要进行强转,这里的_Str是要传地址的!
	ofs.write((const char*)& p, sizeof(p));
	
	// 5、关闭文件
	ofs.close();
}

02 读文件主要利用流对象调用成员函数 read

函数原型:istream& read(char *buffer, long len);

参数解释:字符指针 buffer 指向内存中一段存储空间,len 是读写的字节数

#include <fstream>
#include <string>

class Person
{
    
    
public:
	char m_Name[64];
	int m_Age;
};

void test01()
{
    
    
    	ifstream ifs("person.txt", ios::in | ios::binary);
	if (!ifs.is_open())
	{
    
    
		cout << "文件打开失败" << endl;
	}

	Person p;
	ifs.read((char *)&p, sizeof(p));  // 这里 read 的第一个参数也是要传地址的!

	cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}

猜你喜欢

转载自blog.csdn.net/Coder_J7/article/details/130798291
今日推荐