文章目录
C++核心编程
1 内存分区模型
C++程序在执行时,将内存大方向划分为 4个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理
- 全局区:存放全局变量和静态变量 以及 常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配与释放,若程序员不是放,程序结束时由操作系统回收
目的与意义:不同区域存放的数据,赋予不同的生命周期,让编程更加灵活
1.1 程序运行前
在程序预处理 编译 汇编 链接后,生成了 可执行文件,未执行该程序前 C++内存空间分为两个区域:
-
代码区:
- 作用:存放CPU执行的机器指令
- 特点:
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外的 修改了它的指令
-
全局区:
- 作用:存放全局变量与静态变量,包含常量区(放字符串常量和
const
修饰的其它常量) - 该区域的数据在程序结束后 由操作系统释放
- 作用:存放全局变量与静态变量,包含常量区(放字符串常量和
全局静态数据区(全局变量,静态变量,字符串常量,const修饰的全局常量), 代码区(共享 只读)
#include <iostream>
using namespace std;
//全局变量
int g_a = 1, g_b = 1;
//全局静态常量
static int s_g_a = 1,s_g_b = 1;
// 全局常量
const int c_g_a = 1, c_g_b = 1;
int main(){
/*******************普通局部变量********************/
int l_a = 1, l_b = 1;
cout <<"局部变量l_a的地址为: "<< &l_a << endl;
cout <<"局部变量l_b的地址为:" << &l_b << endl<< endl;
/*******************全局变量********************/
cout <<"全局变量g_a的地址为: "<< &g_a << endl;
cout <<"全局变量g_b的地址为:" << &g_b << endl<< endl;
/******************局部静态变量*******************/
static int s_a = 1, s_b = 1;
cout <<"局部静态变量s_a的地址为: "<< &s_a << endl;
cout <<"局部静态变量s_b的地址为:" << &s_b << endl<< endl;
/******************全局静态变量*******************/
cout <<"全局静态变量s_g_a的地址为: "<< &s_g_a << endl;
cout <<"全局静态变量s_g_b的地址为:" << &s_g_b << endl<< endl;
/* ******************常量*******************
常量: 字符串常量 + const修饰的变量
const修饰的变量: const修饰的全局变量(在常量区),const修饰的局部变量(不在常量区)
*/
cout <<"字符串常量的地址为:" << &("Hello World") << endl;
cout <<"全局常量c_g_a的地址为: "<< &c_g_a << endl;
cout <<"全局常量c_g_b的地址为: "<< &c_g_b << endl;
const int c_l_a = 1, c_l_b = 1;
cout <<"局部常量c_l_a的地址为: "<< &c_l_a << endl;
cout <<"局部常量c_l_b的地址为: "<< &c_l_b << endl;
system("pause");
return 0;
}
总结:
- C++在程序运行前 分为全局区和代码区
- 代码区特点是共享与只读
- 全局区中存放全局变量、静态变量、常量
- 常量区中存放const修饰的全局常量和字符串常量
1.2 程序运行后
栈区
- 由编译器自动释放,存放函数的参数值,局部变量等
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
#include<iostream>
using namespace std;
int * func(){ // 如果有形参,形参数据也会放在栈区
int a = 0; // 局部变量 存放在栈区,栈区的数据在函数执行完后自动释放
return &a;
}
int main(){
int *p = func();
cout << *p << endl; //解引用打印出局部变量a的值
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
/*
使用我的编译,在运行时候直接报错
老师的编译器可以运行成功,但只有第一次打印输出值是正确的,其它错误
- 第一次输出是正确的,因为编译器给我们做了一次保留
- 但是后面 第二次 第三次等,数据就不再保留
局部变量的内存在超出其函数作用域后,已经被释放
此时 *p 已经变成了 野指针
*/
堆区
-
由程序员分配释放,若程序员不释放,程序结束时 由操作系统回收
-
在C++ 中主要利用 操作符 new 在堆区中开辟内存
#include <iostream>
using namespace std;
int * func(){
int *a = new int (10);
// 让指针p指向堆中申请的内存区域,内存区域大小是整形大小,存放的数据是10
// 指针依旧是局部变量,也是放在栈上,指针保存的数据放在堆区
return a;
}
int main(){
int *p = func();
cout << *p << endl; //解引用打印出局部变量a的值
cout << *p << endl;
cout << *p << endl;
delete p; //释放堆中申请的资源
system("pause");
return 0;
}
/*
函数返回堆的内存地址
因为堆需要程序员手动释放,因此 在函数作用域外 堆内存地址依然存在
用p指针获得堆的内存地址,并用*p解引用 获得堆内存中存放的数据10
*/
![](https://img-blog.csdnimg.cn/46b00c7008694997b3ef7614a63b9d0f.png)
1.3 new 操作符
new 操作符
- C++ 利用new操作符在堆中开辟数据
- 堆区中开辟的数据,需要由程序员手动开辟,手动释放,释放利用操作符delete
- 语法:
new 数据类型
- 利用new创建的数据,会返回该数据对应的 类型指针
#include <iostream>
using namespace std;
int main(){
int* p = new int(5); // 分配一个 int 对象并初始化为 5
int* arr = new int[10]; // 分配一个包含 10 个 int 元素的数组
// 使用分配的内存
*p = 10;
cout << *p << endl;
for(int i = 0; i < 10; i++) arr[i] = i + 100;
for(int i = 0; i < 10; i++) cout << arr[i] << " ";
// 释放内存
delete p;
delete[] arr; // 不加上中括号,则只会释放一个数据,不会释放整个数组的元素
system("pause");
return 0;
}
2 引用
2.1 引用的基本使用
作用:给一个变量起别名
语法:数据类型 &别名 = 原名;
int a = 10; // 四个字节大小的内存,内存中存放的数据是10,用a去代表这块内存,方便操作内存
int &b = a; // 给这块内存取一个别名,为 b
printf("a = %d, b = %d\n", a, b); // 输出 a = 10, b = 10
b = 20;
printf("a = %d, b = %d\n", a, b); // 输出 a = 20, b = 20 , 因为原名a 和别名操作同一块内存
![](https://img-blog.csdnimg.cn/1408df7847584eed967f6228bd752fe9.png)
原名 和 别名,修改 操作的是同一块内存
2.2 引用注意事项
- 引用必须初始化
- 引用在初始化后,不可以再发生改变
//int &b; //报错,引用 变量 "b" 需要初始值设定项
int a = 10, c = 20;
int &b = a;
//&b = c;// 报错,表达式必须是可修改的左值
b = c;// 这是赋值操作
2.3 引用作为函数参数
值传递,地址传递,引用传递
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针 修改实参
// 举例: 值传递,地址传递,引用传递 进行交换两个数
#include <iostream>
using namespace std;
void swap_Byvalue(int a, int b){
int tmp = a;
a = b;
b = tmp;
}
void swap_Bypointer(int *a, int *b){
int tmp = *a;
*a = *b;
*b = tmp;
}
void swap_Byreference(int &a, int &b){ // a是num1的别名,b是num2的别名
int tmp = a; // 原名和别名操作修改的是同一块内存
a = b;
b = tmp;
}
int main(){
int num1 = 10, num2 = 20;
swap_Byvalue(num1, num2);
printf("num1 = %d, num2 = %d swap_Byvalue\n", num1, num2); // 10 20
num1 = 10, num2 = 20;
swap_Bypointer(&num1, &num2);
printf("num1 = %d, num2 = %d swap_Bypointer\n", num1, num2); // 20 10
num1 = 10, num2 = 20;
swap_Byreference(num1, num2);
printf("num1 = %d, num2 = %d swap_Byreference\n", num1, num2); // 20 10
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/cc5369d262524054b761813f09d87680.png)
总结:通过引用参数 产生的效果桶地址传递是引用的,引用的语法更加清楚 更加简单
比较 值传递,传递地址(指针),传递引用 的优缺点
C++中,函数参数传递可以通过值传递、传递地址(指针)和传递引用三种方式进行。
下面是各自的特点
值传递
缺点:
无法修改实参值,只能修改函数参数副本 即形参
如果传递的是大型对象,会产生额外的复制开销,影响性能
传递地址
优点:
可以修改实参值
可以传递空指针作为参数,用于表示没有有效的对象
可以通过传递 指针数组(数组每个元素都是指针) 等实现多个返回值
缺点:
修改数据需要函数内部解引用
*a = 10;
,可能会导致空指针引用和悬挂指针错误需要手动管理 指向动态分配内存的指针的生命周期,包括分配和释放动态内存
指针操作,容易出错
传递引用
- 优点:
- 可以修改实参值
- 可以直接访问和修改 传递的变量,类似于 对原始变量的操作
- 不存在空引用,因为引用必须初始化为有效对象
- 缺点:
- 无法传递数组,因为引用的长度是固定的
- 可能会意外修改传递的变量的值
- 不支持空引用,则需要始终传递有效的对象
如果需要在函数内部修改实参,可以选择传递地址或者传递引用。
如果需要传递动态分配的内存,可以选择传递地址
如果需要频繁修改实参的值 且 不希望出现空引用问题,可以选择传递引用
如果不需要在函数内部修改实参的值,可以选择值传递
2.4 引用作为函数返回值
作用:引用是可以作为函数的返回值存在
注意:不要返回局部变量引用
用法:函数调用作为左值
-
案例:将局部变量的引用作为函数返回值,报错!
#include <iostream> using namespace std; int& func(){ int a = 10; // 局部变量放在栈区 生命周期只在该函数内 return a; } int main(){ int &ref = func(); cout << ref << endl; // 运行报错 warning: reference to local variable 'a' returned cout << ref << endl; system("pause"); return 0; } /* 老师的编译器同样可以输出,第一次结果正确 后几次的输出结果随机,因为别名的内存已经被释放,第一次成功是因为编译器的暂时保留 */
-
举例:将函数调用作为等号左边的值,被操作
#include <iostream> using namespace std; int& func1(){ static int a = 10; // 静态变量放在全局/静态数据区中,生命周期随程序,不随函数 return a; } int main(){ int &ref = func1(); cout << ref << endl; // 10 cout << ref << endl; // 10 func1() = 100; // 本质就是 a = 100 cout << ref << endl; // 100 ref是a的别名,因此ref a func1() 都是对同一块内存的名字 cout << ref << endl; // 100 system("pause"); return 0; }
2.5 引用的本质
本质:引用的本质在C++内部的实现是指针常量,int * const p
修饰的是p
的值,即 p
保存的地址值不能改变(p的指向不变),但是 p
保存的地址值指向数据的数据值可以改变
int a = 10;
int &ref = a; // 自动转换为 int * const ref = &a;
ref = 20; // 发现ref是引用,自动转换为 *ref = 20;
引用是对指针进行了封装,底层依然是通过指针实现,引用占用的内存和指针占用的内存长度一样,32位环境4 个字节,64位环境下8个字节,之所以不能获取引用的地址,是因为编译器进行了内部转换。
引用的本质,就是一个指针常量,因此引用(指针的朝向)一旦被初始化后,就不可以发生改变
引用和指针的区别
- 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定 义时不必赋值,以后也能指向任意数据。
- 指针可以用多级
int **p
,但是引用只有一级int &&a
不合法,可以int a = 10; int &b = a; int &c = b;
- 指针与引用的 自增自减 运算符意义不同,指针的
++
--
操作表示指向下一份数据,引用的++
--
操作则表示 其所指代数据值得++
--
2.6 常量引用
作用:用来修饰形参,防止误操作时,形参改变实参
#include <iostream>
using namespace std;
void showValue(const int &val){
// val = 30; // 报错:表达式必须是可修改的左值
cout << val << endl;
}
int main(){
// int &ref = 10; //报错:非常量引用的初始值必须为左值
// 加上const之后,编译器将代码修改为int tmp = 10; const int &ref = tmp;
const int &ref = 10;
// ref = 20; //报错:表达式必须是可修改的左值。加上const之后,不可以再被修改
// 真正的使用场景 是用来修饰函数形参
int a = 10;
showValue(a);
system("pause");
return 0;
}
-
int &ref = 10; // 报错
-
在函数中形参为
const int &val
的情况下,修改形参(实参的别名),报错
3 函数提高
3.1 函数默认参数 (形参的默认值)
C++中,函数形参列表可以有默认值
语法:
返回值类型 函数名(参数=默认值){
函数体...
return 返回值
}
举例:
#include <iostream>
using namespace std;
int func(int a, int b = 10, int c = 10){
return a + b + c;
}
/*
注意事项:
1. 如果某个位置设置了默认参数,那么从该位置往后的所有形参都必须有默认值
int func1(int a, int b = 10, int c, int d){
}
2. 如果函数声明有默认参数,函数实现就不能有默认参数
声明和实现只能有一个默认参数
*/
int func2(int a = 10, int b = 20);
int func2(int a = 10, int b = 20){
return a + b;
}
int main(){
cout << func(10, 20, 30) << endl;
cout << func(10) << endl; // 可以只给一个值 即函数形参a的值,b和c使用形参默认值
cout << func(10, 20) << endl;
// 如果在函数调用时传入了自己的数据,就用自己的数据,否则使用形参默认值;
cout << func2(1,2) << endl; //运行报错 func2()定义处 重复定义默认参数
system("pause");
return 0;
}
注意事项1:如果某个位置设置了默认参数,那么从该位置往后的所有形参都必须有默认值
注意事项2:如果函数声明有默认参数,函数实现就不能有默认参数,声明和实现只能有一个默认参数
3.2 函数占位参数
C++ 函数的形参列表,可以有占位参数,用来占位,调用函数时 必须填补该位置
语法:返回值类型 函数名 (数据类型) { ... }
#include <iostream>
using namespace std;
// 占位参数
void func(int a, int){
cout <<"this is a func"<< endl;
}
int main(){
// func(10); // 报错,函数调用中的参数太少
func(10,10); // 占位参数必须填补, 函数调用的传入值无法在函数中被使用(后面课程中会用到
system("pause");
return 0;
}
3.3 函数重载
3.3.1 函数重载概述
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同,或者个数不同 或者 顺序不同
#include <iostream>
using namespace std;
// 同一个作用域下; 函数名称相同函数参数类型不同,或者个数不同 或者 顺序不同
void func(){
cout <<"func()的调用" <<endl;
}
void func(int a){
cout <<"func(int a)的调用" <<endl;
}
void func(double a){
cout <<"func(double a)的调用" <<endl;
}
void func(int a, double b){
cout <<"func(int a, double b)的调用" <<endl;
}
void func(double a, int b){
cout <<"func(double a, int b)的调用" <<endl;
}
// int func(double a, int b){
// cout <<"func(double a, int b)的调用" <<endl;
// return a + b;
// } //报错:无法重载仅按返回类型区分的函数,会产生歧义
/*
注意事项:函数的返回值不可以作为函数重载的条件
*/
int main(){
func();
func(10); // 函数参数 个数不同
func(3.14); // 函数参数 类型不同
func(10,3.14);
func(3.14,10);//函数参数 顺序不同
system("pause");
return 0;
}
- 输出结果:
![](https://img-blog.csdnimg.cn/cee676d0d7ae4624bf71ec29dcdd4348.png)
- 不可以根据返回值类型 实现函数重载
3.3.2 函数重载的注意事项(有无const引用,默认参数)
-
引用 作为重载条件 ,可以
void func(int &a){ } void func(const int &a){} // 引用形参加不加 const 可以构成重载条件 func(10);// 调用有const版本 int tmp = 10; const int &a = tmp; int a = 10; func(a);//调用无const版本
-
函数重载碰到 函数默认参数,不可以
void func2(int a){ } void func2(int a, int b = 10){ } func2(10); //造成二义性,编译器报错
举例:
#include <iostream>
using namespace std;
// 同一个作用域下; 函数名称相同函数参数类型不同,或者个数不同 或者 顺序不同
void func(int &a){
cout <<"func(int &a)的调用" <<endl;
}
void func(const int &a){
cout <<"func(const int &a)的调用" <<endl;
}
/*****func(int &a) 和 func(const int &a) 构成重载,因为参数类型不同******/
void func2(int a){
cout <<"func2(int a)的调用" <<endl;
}
void func2(int a, int b = 10){
cout <<"func2(int a, int b = 10)的调用" <<endl;
}
int main(){
int a = 10;
func(a); //重载会调用无const版本,因为a作为变量 应该可读可写
func(10); // 重载会调用有const版本
// int &a = 10; // 不合法
// const int &a = 10; -> int tmp = 10; const &a = tmp;
//func2(10); // 既可以调func2(int a),也可以调func2(int a, int b = 10)
// 出现二义性,编译器报错:有多个 重载函数 "func2" 实例与参数列表匹配
// 建议写函数重载的时候,不要使用默认参数
func2(10,8);
system("pause");
return 0;
}
-
答案:
-
重载和默认参数,容易构成二义性
4 类和对象
C++ 面向对象的三大特性:封装、继承、多态
C++ 认为万事万物 都皆为对象,对象上有属性(成员变量)和行为(成员方法)
4.1 封装
4.1.1 封装的意义
尽量隐藏类的内部实现,只想用户提供有用的成员函数。
封装的意义
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法: class 类名{ 访问权限: 成员变量/成员方法 };
类中的属性和行为,统一称为成员
属性:成员属性 成员变量
行为:成员函数 成员方法
成员变量大都以 m_
开头,这是约定成俗的写法,不是语法规定的内容。以 m_
开头既可以一眼看出这是成员变 量,又可以和成员函数中的形参名字区分开。
-
意义一:将属性和行为作为一个整体,表现生活中的事物
-
示例1:设计一个圆类,求圆的周长
#include <iostream> using namespace std; const double PI = 3.14; // 设计一个圆类,求圆的周长:2 * PI * 半径 class Circle{ public: // 访问权限 double m_r; // 属性 double calculateZC(){ // 行为 return 2 * m_r * PI; } }; int main(){ // 实例化:通过一个类,创建一个对象的过程 Circle c; // 通过圆类,创建具体的圆(对象) c.m_r = 2.0; // 给圆对象的属性进行赋值 printf("圆的半径为%f,周长为%f\n",c.m_r,c.calculateZC()); system("pause"); return 0; }
-
示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
#include <iostream> using namespace std; class Student { private: string m_name; // 姓名 int m_id; // 学号 public: void set_name(string name){ m_name = name; } void set_id(int id){ m_id = id; } void showInfo(){ printf("学生姓名为%s,学生学号为%d\n", m_name.c_str(),m_id); } }; int main(){ Student std; std.set_id(1); std.set_name("小红"); std.showInfo(); Student std1; std.set_id(2); std.set_name("小蓝"); std.showInfo(); system("pause"); return 0; }
-
-
意义2:类在设计的时候,可以把属性和方法放在不同的权限下,加以控制
访问权限有三种:
- public 公共权限 类内可以访问 类外也可以访问
- protected 保护权限 类内可以访问 类外不可以访问 (继承的子类可以访问父类的protected成员)
- private 私有权限 类内可以访问 类外不可以访问 (子类也无法访问父类的private成员)
如果不声明权限,则使用默认权限private
4.1.2 struct 和 class 的区别
C++ 中struct 和 class 唯一区别在于,默认的访问权限不同
- 默认访问权限:
- struct 作为数据结构的实现体,默认的数据访问控制是 public 的
- class 作为对象的实现体,默认的成员变量访问控制是 private 的
- 默认继承权限:class 默认 private, struct 默认是public
/**************** 此处说明struct和class的默认访问权限的不同 *******************/
#include <iostream>
using namespace std;
// struct T1 和 class T2 在定义属性的时候,没有给权限,则使用默认权限
struct T1{
int a;
};
class T2{
int a;
};
int main(){
T1 t1;
T2 t2;
t1.a = 10; // struct T1的默认权限 可以被访问 -> public
//t2.a = 10; // class T2的默认权限,不可以被访问 -> private
// 报错:成员 "T2::a" (已声明 所在行数:10) 不可访问
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/d3dcf0030b56467d805a4051bd7070a5.png)
4.1.3 成员属性设置为私有
- 将所有成员属性设置为私有,可以自己控制 成员属性 的控制读写权限
- 对于写权限, 我们可以检测数据的有效性
#include <iostream>
using namespace std;
class Person{
private:
string m_name; // 可读可写
int m_age = 0; // 可读可写
string m_lover; // 可写
public:
/********* 对私有成员属性,提供公共控制接口 **********/
// m_name 可读可写
void setName(string name){
m_name = name;
}
string getName(){
return m_name;
}
// age 可读可写
void setAge(int age){
if(age >= 0 && age <= 150) m_age = age;
}
int getAge(){
return m_age;
}
// lover 只写
void setLover(string lover){
m_lover = lover;
}
};
int main(){
Person p;
p.setName("小红");
p.setLover("小蓝");
p.setAge(30);
printf("姓名:%s,年龄:%d\n",p.getName().c_str(),p.getAge()); // p.m_lover 无法获取
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/2d1b8f5ff8984688b55139ef993b2044.png)
-
练习案例1:设计立方体类,求立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等
#include <iostream> using namespace std; class Cube{ private: double m_L = 0; double m_H = 0; double m_W = 0; public: double calVolume(){ return m_L*m_H*m_W; } double calArea(){ return 2 * ( m_L * m_H + m_H * m_W + m_L * m_W); } bool isEqual(Cube &c){ //也可以:if(m_L == c.getL() && m_H == c.getH() && m_W == c.getW()) if(m_L == c.m_L &&m_H ==c.m_H && m_W == c.m_W ) return true; else return false; } void setL(double L){ m_L = L; } void setH(double H){ m_H = H; } void setW(double W){ m_W = W; } double getL(){ return m_L; } double getH(){ return m_H; } double getW(){ return m_W; } }; bool isEqual(Cube &a, Cube &b){ // 减少形参的拷贝,直接用引用 if(a.getH() == b.getH() && a.getL() == b.getL() && a.getW() == b.getW()) return true; return false; } // bool isEqual(Cube a, Cube b){ // if(b.m_L == b.m_L &&b.m_L ==b.m_H && b.m_L == b.m_W ) return true; // else return false; // } //成员 "Cube::m_L" (已声明 所在行数:6) 不可访问 int main(){ Cube a,b; a.setH(10), a.setL(10), a.setW(10); printf("Cube a,长%.2f宽%.2f高%.2f,体积%.2f 面积%.2f\n",a.getL(),a.getW(),a.getH(),a.calVolume(),a.calArea()); b.setH(10),b.setL(10),b.setW(10); printf("Cube b,长%.2f宽%.2f高%.2f,体积%.2f 面积%.2f\n",b.getL(),b.getW(),b.getH(),b.calVolume(),b.calArea()); cout << "Cube a 和 Cube b 是否相等(成员函数):" << b.isEqual(a) << endl; cout << "Cube a 和 Cube b 是否相等(全局函数):" << isEqual(a,b) << endl<< endl; b.setH(20),b.setL(10),b.setW(10); printf("Cube b,长%.2f宽%.2f高%.2f,体积%.2f 面积%.2f\n",b.getL(),b.getW(),b.getH(),b.calVolume(),b.calArea()); cout << "Cube a 和 Cube b 是否相等(成员函数):" << b.isEqual(a) << endl; cout << "Cube a 和 Cube b 是否相等(全局函数):" << isEqual(a,b) << endl; system("pause"); return 0; }
![](https://img-blog.csdnimg.cn/618cdb1cd5e54bc787349438efb4b15f.png)
成员方法:bool isEqual(Cube &c)
中,可以访问c的私有成员变量,为什么?C++ 允许同一个类的不同对象(实例)访问彼此的私有成员。
体现了面向对象的思想:封装是针对类 而不是对象,相同类之间所有的成员都是public,在类的成员函数中可以访问同类型实例对象的私有成员
-
练习案例2:电和圆的关系。设计一个圆形Cirle类,和一个点Point类,设计点和圆的关系(圆内 圆上 圆外)
点 到 圆心距离 == 半径, 点在圆上
点 到 圆心距离 < 半径, 点在圆内
点 到 圆心距离 > 半径, 点在圆外
计算 点 到 圆心距离 :两点距离公式
#include <iostream> using namespace std; class Point{ private: double m_x; double m_y; public: void setX(double x){ m_x = x; } void setY(double y){ m_y = y; } double getX(){ return m_x; } double getY(){ return m_y;} }; class Circle{ private: Point m_center; // 在类中,让另一个类 作为本类的成员 double m_r; // 圆的半径 public: void setR(double r){m_r = r;} double getR(){ return m_r; } void setCenter(double x, double y){ m_center.setX(x); m_center.setY(y); } void setCenter(Point center){ m_center = center; } Point getCenter(){ return m_center; } }; void isInCircle(Point p, Circle c){ double x1 = p.getX(), y1 = p.getY(); double x2 = c.getCenter().getX(), y2 = c.getCenter().getY(); double dis = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2); // 计算p和圆心距离 (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) 和 r*r 比较 if(dis == c.getR()*c.getR()){ cout << "点在圆上" <<endl; }else if(dis < c.getR()*c.getR()){ cout << "点在圆内" <<endl; }else{ cout << "点在圆外" <<endl; } } int main(){ Circle c; c.setCenter(10,10); c.setR(3); Point p; p.setX(10.5), p.setY(11); isInCircle(p,c); p.setX(20), p.setY(20); isInCircle(p,c); p.setX(10), p.setY(7); isInCircle(p,c); system("pause"); return 0; }
![](https://img-blog.csdnimg.cn/09c3e1d16b71441b9dbec6d48e8d2127.png)
在类中,让另一个类 作为本类的成员
可以将声明 和 定义 分开写到.h 和 .cpp 文件中
以下是分开到不同的.h 和 .cpp 的形式:
/**************************point.h*******************************/
#pragma once //防止头文件重复包含
#include <iostream>
using namespace std;
class Point{
private:
double m_x;
double m_y;
public:
void setX(double x);
void setY(double y);
double getX();
double getY();
};
/**************************point.cpp*******************************/
#include "point.h"
// 需要告知是Point类下边的成员函数,不是一个普通函数
void Point::setX(double x){ m_x = x; }
void Point::setY(double y){ m_y = y; }
double Point::getX(){ return m_x; }
double Point::getY(){ return m_y;}
/**************************circle.h*******************************/
#pragma once //防止头文件重复包含
#include "point.h"
class Circle{
private:
Point m_center; // 在类中,让另一个类 作为本类的成员
double m_r; // 圆的半径
public:
void setR(double r);
double getR();
void setCenter(double x, double y);
void setCenter(Point center);
Point getCenter();
};
/**************************circle.cpp*******************************/
#include "circle.h"
void Circle::setR(double r){m_r = r;}
double Circle::getR(){ return m_r; }
void Circle::setCenter(double x, double y){
m_center.setX(x);
m_center.setY(y);
}
void Circle::setCenter(Point center){
m_center = center;
}
Point Circle::getCenter(){
return m_center;
}
/************************** 主函数:main.cpp*******************************/
#include <iostream>
#include "circle.h"
#include "point.h"
#include "circle.cpp"
#include "point.cpp"
using namespace std;
void isInCircle(Point p, Circle c){
double x1 = p.getX(), y1 = p.getY();
double x2 = c.getCenter().getX(), y2 = c.getCenter().getY();
double dis = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2);
if(dis == c.getR()*c.getR()){
cout << "点在圆上" <<endl;
}else if(dis < c.getR()*c.getR()){
cout << "点在圆内" <<endl;
}else{
cout << "点在圆外" <<endl;
}
}
int main(){
Circle c;
c.setCenter(10,10);
c.setR(3);
Point p;
p.setX(10.5), p.setY(11);
isInCircle(p,c);
p.setX(20), p.setY(20);
isInCircle(p,c);
p.setX(10), p.setY(7);
isInCircle(p,c);
system("pause");
return 0;
}
4.2 对象的初始化和清理
对象的初始化:创建对象时对其进行设置和准备的过程,类似于电子产品的出厂设置
对象的清理:在对象不再被使用时对其进行释放和销毁的过程
4.2.1 构造函数和析构函数
C++ 中 对象初始化——构造函数,对象清理——析构函数
两个函数将被编译器自动调用,完成对象初始化和清理的工作
如果自己不提供构造函数和析构函数,编译器会提供默认的空实现的构造函数和析构函数
-
构造函数:创建对象时 为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
- 语法:
类名(){ }
- 构造函数,没有返回值,也不写void
- 函数名称 和 类名 相同
- 构造函数可以有参数,因此可以发生重载
- 程序在创建对象时,自动调用构造函数,无需手动调用, 且只调用一次
- 语法:
-
析构函数:对象销毁前,系统自动调用,执行一些清理工作
- 语法:
~类名(){}
- 析构函数,没有返回值,不写void
- 函数名称和类名相同,在名称前加上符号
~
- 析构函数不可以有参数,因此不会发生重载
- 程序在对象销毁前 会自动调用析构,无需手动调用,且只会调用一次
- 语法:
#include <iostream>
using namespace std;
class Person{
public:
Person(){ //没有返回值没有void;有参数,可以发生重载
cout <<"Person 构造函数调用" << endl;
}
~Person(){ //没有返回值没有void;没有参数,不可以发生重载
cout <<"Person 析构函数调用" << endl;
}
};
void func(){
Person ccc; // p 随着函数结束而结束
}
int main(){
func();
Person p; // 对象p随着main函数的结束而结束
system("pause");
return 0;
}
func()
函数内的对象是局部变量,在栈中,随着函数结束 变量会被销毁 弹出栈
因此在func()
中,在创建对象的时候调用析构函数,对象销毁前调用析构函数
![](https://img-blog.csdnimg.cn/8de2d10af8a24838be0917fd38b3dd5a.png)
4.2.2 构造函数的分类及调用
两种分类方式:
- 按照参数分类:有参构造 和 无参构造
- 按照类型分类:普通构造 和 拷贝构造 // 除了拷贝构造函数以外的函数都是普通构造
三种调用方式:括号法;显示法;隐式转换法
#include <iostream>
using namespace std;
class Person{
public:
int age = 0;
/************ 构造函数的分类 与 调用
************ 按照参数分类:有参构造 和 无参(默认)构造
*********** 按照类型分类:普通构造 和 拷贝构造 */
Person(){
cout <<"Person的构造函数调用\n";
}
Person(int a){
age = a;
cout <<"Person的有参构造函数调用\n";
}
Person(const Person &p){ // 克隆一份数据, cosnt表示不可以改变本体,用引用 减少开销
// 将传入的人身上的所有属性,拷贝到当前对象上
age = p.age;
cout <<"Person的拷贝构造函数调用\n";
}
~Person(){
cout <<"Person的析构函数调用\n";
}
};
void func(){
// 1. 括号法
printf("********************括号法********************\n");
Person p1; // 默认构造函数的调用
Person p2(10); // 有参构造函数的调用
Person p3(p2); // 拷贝构造函数
cout <<"p1的年龄"<< p1.age << endl;
cout <<"p2的年龄"<< p2.age << endl;
cout <<"p3的年龄"<< p3.age << endl;
// 注意事项:调用默认构造函数的时候,不要加小括号
// Person p4(); // 没有报错,但没有创建出来对象,编译器认为是一个函数的声明,不会认为是在创建对象
// void func(); 类似于下面的函数声明
//2. 显示法
printf("********************显示法********************\n");
Person a1; // 默认构造函数
Person a2 = Person(10); // 有参构造函数
Person a3 = Person(a2); // 拷贝构造函数
Person(10); // 匿名对象 特点:当前行执行结束后,系统会立即回收匿名对象
cout <<"匿名对象在当前行结束后,先析构再执行下面的代码 \n";
// 注意事项:不要利用拷贝构造函数,初始化匿名对象
Person(a3); // 运行报错:a3被重定义 error: redeclaration of 'Person a3'
// 因为编译器会认为是对象声明,即Person(a3) == Person a3; 无参构造对象a3 名称对象,重定义
//3. 隐式转换法
printf("********************隐式转换法法********************\n");
Person b1 = 10; // 相当于写了 Person b1 = Person(10); 有参构造
Person b2 = b1; // 拷贝构造
// 通过使用关键字explicit显示声明构造函数,组织对象和内置类型的隐式转换
}
int main(){
func();
system("pause");
return 0;
}
-
结果:
-
报错:利用拷贝构造函数 初始化匿名对象
通过explicit关键字显示声明构造函数,阻止对象和内置类型的隐式转换
#include <iostream>
using namespace std;
class Person{
public:
int age = 0;
Person(){
cout <<"Person的构造函数调用\n";
}
// 通过使用关键字explicit显示声明构造函数,阻止对象和内置类型的隐式转换
explicit Person(int a){
age = a;
cout <<"Person的有参构造函数调用\n";
}
~Person(){
cout <<"Person的析构函数调用\n";
}
};
int main(){
Person b1 = 10; //报错
system("pause");
return 0;
}
报错:
![](https://img-blog.csdnimg.cn/2f7452046ef247dab056127dda0d8bf3.png)
4.2.3 拷贝构造函数调用时机
一共有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新的对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
Person(){
cout << "Person 默认构造函数" << endl;
}
Person(int age){
m_age = age;
cout << "Person 有参构造函数" << endl;
}
Person(const Person &p){
m_age = p.m_age;
cout << "Person 拷贝构造函数" << endl;
}
~Person(){
cout << "Person 析构函数" << endl;
}
};
// 1- 使用一个已经创建完毕的对象来初始化一个新的对象
void func1(){
cout << "func1:" << endl;
Person p1(10); // 有参构造函数
Person p2(p1); // 拷贝构造函数
cout << "p2 age:" << p2.m_age << endl;
}
// 2- 值传递的方式给函数参数传值
void work2(Person p){
}
void func2(){
cout << "func2:" << endl;
Person p(20); // 有参构造函数
work2(p); // 拷贝构造函数,实参传递给形参的时候,会调用拷贝构造函数
}
// 3- 以值方式返回局部对象
Person work3(){
Person p1; // 默认构造函数
cout << &p1 << endl;
return p1;
}
void func3(){
cout << "func3:" << endl;
Person p = work3(); // 拷贝构造函数
cout << &p << endl;
/* 我的编译器使用了返回值优化
返回值优化:直接将 p1 对象放在 p 中,而不是通过调用拷贝构造函数来创建一个新的对象。
因此,没有调用拷贝构造函数。
*/
}
int main(){
func1();
func2();
func3();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/172bf3be005f411a80c6c17945838682.png)
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加4个默认函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 拷贝构造函数 (创建新对象,且对根据传入对象的属性 对该对象的属性进行值拷贝)
- 赋值运算符函数 (不算构造函数,但是算缺省函数)(将一个对象的值 赋值给 一个已存在的对象)
class MyClass {
public:
// 默认构造函数: 没有参数,通常用于对象的默认初始化
MyClass::MyClass() {
// 构造函数的代码
}
// 拷贝构造函数: 创建一个新对象并将其初始化为已有对象的副本
MyClass(const MyClass& other){
// 拷贝构造函数的代码
}
// 拷贝赋值运算符: 将一个对象的值赋给另一个已存在的对象
MyClass& operator=(const MyClass& other){
if (this != &other) {
// 拷贝赋值运算符的代码
}
return *this;
}
// 析构函数: 在对象被销毁时自动调用,用于执行对象的清理操作
~MyClass(){
// 析构函数的代码
}
};
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供 默认拷贝构造函数
- 如果用户定义拷贝构造函数,C++ 不再提供其它构造函数
-
用户定义有参构造函数 和 无参构造函数,用户不提供拷贝构造函数,编译器会提供默认构造函数
#include <iostream> using namespace std; class Person { public: int m_age; Person(){ cout << "Person 默认构造函数" << endl; } Person(int age){ m_age = age; cout << "Person 有参构造函数" << endl; } // Person(const Person &p){ // m_age = p.m_age; // cout << "Person 拷贝构造函数" << endl; // } ~Person(){ cout << "Person 析构函数" << endl; } }; void func1(){ Person p; p.m_age = 10; Person p1(p); // 注释掉自己写的拷贝构造函数,查看p1的年龄属性是否正确 cout << p1.m_age << endl; } int main(){ func1(); system("pause"); return 0; }
![](https://img-blog.csdnimg.cn/f5b55798ec174988bed40bfebf2e63be.png)
-
用户定义有参构造函数,系统不再提供 无参构造函数,但依然提供拷贝构造函数
#include <iostream> using namespace std; class Person { public: int m_age; // Person(){ // cout << "Person 默认构造函数" << endl; // } Person(int age){ m_age = age; cout << "Person 有参构造函数" << endl; } // Person(const Person &p){ // m_age = p.m_age; // cout << "Person 拷贝构造函数" << endl; // } ~Person(){ cout << "Person 析构函数" << endl; } }; void func1(){ Person p; p.m_age = 10; Person p1(p); } int main(){ func1(); system("pause"); return 0; }
-
用户定义有参构造函数,系统不再提供 无参构造函数
-
但依然提供拷贝构造函数,即
p2
的m_age
属性值是10
-
-
如果用户定义拷贝构造函数,C++ 不再提供其它构造函数
#include <iostream> using namespace std; class Person { public: int m_age; // Person(){ // cout << "Person 默认构造函数" << endl; // } // Person(int age){ // m_age = age; // cout << "Person 有参构造函数" << endl; // } Person(const Person &p){ m_age = p.m_age; cout << "Person 拷贝构造函数" << endl; } ~Person(){ cout << "Person 析构函数" << endl; } }; void func1(){ Person p; // 报错:类 "Person" 不存在默认构造函数 Person p1(10); // 报错:没有与参数列表匹配的构造函数,用户定义的有参构造函数 Person p2(p1); // 系统默认的拷贝构造函数 cout << p2.m_age << endl; } int main(){ func1(); system("pause"); return 0; }
总结:
- 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供 默认拷贝构造函数
- 如果用户定义拷贝构造函数,C++ 不再提供其它构造函数
4.2.5 深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:堆区中 重新申请内存空间,进行拷贝操作
使用编译器提供的拷贝构造函数是浅拷贝:
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
int* m_height;
Person(){
cout << "Person 默认构造函数" << endl;
}
Person(int age, int height){
m_age = age;
m_height = new int(height);// 指针接收堆区申请内存的地址
cout << "Person 有参构造函数" << endl;
}
~Person(){
if(m_height != nullptr) {
delete m_height;
m_height = nullptr;
}
cout << "Person 析构函数" << endl;
}
};
void func(){
Person p1(18,160);
cout <<"p1年龄为:"<<p1.m_age <<",身高为:" <<*p1.m_height<<endl;
Person p2(p1);
cout <<"p2年龄为:"<<p2.m_age <<",身高为:" <<*p2.m_height<<endl;
}
int main(){
func();
system("pause");
return 0;
}
报错:
![](https://img-blog.csdnimg.cn/aa86ad5e25af4603a674794788aaeea3.png)
原理:p2的m_age
和m_height
都是从p1
浅拷贝而来,也就是说p1
和p2
中的m_height
指向的是同一块堆中的内存。因此在func()
中,(栈是后进先出)p2先调用析构函数 释放m_height指向的堆空间,当p1再调用析构函数时,堆空间已经被释放,因此会报错。
解决:自己实现拷贝构造函数 深拷贝,解决浅拷贝带来的问题
让p2的m_height 指向另一片数据相同(都是160),但申请的堆空间不同
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
int* m_height;
Person(){
cout << "Person 默认构造函数" << endl;
}
Person(int age, int height){
m_age = age;
m_height = new int(height);// 指针接收堆区申请内存的地址
cout << "Person 有参构造函数" << endl;
}
Person(const Person &p){
m_age = p.m_age;
// m_height = p.m_height; // 编译器默认实现的就是这行代码
m_height = new int(*p.m_height); // 深拷贝
cout << "Person 拷贝构造函数" << endl;
}
~Person(){
if(m_height != nullptr) {
delete m_height;
m_height = nullptr;
}
cout << "Person 析构函数" << endl;
}
};
void func(){
Person p1(18,160);
cout <<"p1年龄为:"<<p1.m_age <<",身高为:" <<*p1.m_height<<endl;
Person p2(p1);
cout <<"p2年龄为:"<<p2.m_age <<",身高为:" <<*p2.m_height<<endl;
}
int main(){
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/a20e0c11316e4556abbdf1a862c2a5a7.png)
总结:
构造函数中申请了堆的内存,则需要在析构函数中加入 释放堆内存的代码
如果有属性在堆区中开辟,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
编译器提供的默认拷贝构造函数是浅拷贝,如果不利用深拷贝在堆区创建新内存,可能会导致浅拷贝的重复释放堆区问题。
4.2.6 初始化列表
C++ 提供了初始化列表,用来初始化属性
语法:构造函数(): 属性1(值1),属性2(值2){ }
#include <iostream>
using namespace std;
class Person{
public:
int m_A;
int m_B;
int m_C;
/* 传统初始化操作:
Person(int a, int b, int c){
m_A = a;
m_B = b;
m_C = c;
}
*/
// 初始化列表初始化属性
Person(int a, int b, int c):m_A(a),m_B(b),m_C(c){}
};
void func1(){
Person p(1,2,3);
cout <<"m_A:\t"<< p.m_A << "\nm_B:\t"<< p.m_B <<"\nm_C:\t"<< p.m_C <<endl;
}
int main(){
func1();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/34ed02440bc54c43b3b27e2d25261a2b.png)
4.2.7 类对象作为类成员
C++ 类中的成员可以是另一个类的对象, 我们称为该成员为 对象成员
class A {};
class B{
A a;
};
B类中有对象A作为成员,A为对象成员
那么当创建B对象时,A与B的构造和析构函数的顺序是谁先谁后呢?
#include <iostream>
using namespace std;
class Phone{
public:
string m_pName;
Phone(string name){
m_pName = name;
cout << "Phone 构造函数" <<endl;
}
~Phone(){
cout << "Phone 析构函数" <<endl;
}
};
class Person{
public:
string m_name;
Phone m_phone;
// 自动调用Phone的构造函数 Phone m_phone = phoneName; 隐式转换法
Person(string name, string phoneName):m_name(name),m_phone(phoneName){
cout <<" Person 构造函数" <<endl;
}
~Person(){
cout << "Person 析构函数" <<endl;
}
};
// 构造:当其他类的对象作为本类的成员,先构造成员类对象,再构造自身。 先有零件,再有自己
// 析构:析构的顺序与构造相反
void func1(){
Person p("小红","华为");
}
int main(){
func1();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/6888a182fa0b45b3b6b6af72d9999c6a.png)
4.2.8 static 静态成员
静态成员就是在成员变量 和 成员函数前 加上关键字static,称为静态成员
静态成员分为:
-
静态成员变量
-
所有对象都共享同一份数据,属于类 不属于对象 没有this指针
-
在编译阶段分配内存 全局/静态数据区
-
类内声明,类外初始化。必须有一个初始值
#include <iostream> using namespace std; class Person{ public: static int m_A; // 静态成员变量也是有访问权限的 private: static int m_B; }; int Person::m_A = 100; //类内声明,类外初始化 int Person::m_B = 1000; void func1(){ Person p1; cout << p1.m_A <<endl; Person p2; p2.m_A = 200; cout << p1.m_A << endl; cout << p2.m_A << endl; } void func2(){ // 静态成员变量 不属于某个对象 而是属于类,且所有对象都可以访问 // 因此静态成员变量的访问方式有两种 cout << endl; // 1. 通过对象进行访问 Person p; cout << p.m_A << endl; // 2. 通过类名进行访问 cout << Person::m_A << endl; //cout << Person::m_B << endl; // 编译器报错:成员 "Person::m_B" (已声明 所在行数:12) 不可访问 } int main(){ func1(); func2(); system("pause"); return 0; }
-
-
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数 只能访问静态成员变量
#include <iostream> using namespace std; class Person{ public: static int m_A; int m_B; // 静态成员函数 static void funcStatic(){ m_A = 10; // 静态成语函数 访问 静态成员变量,不属于对象 //m_B = 20; // 报错:非静态成员引用必须与特定对象相对,无法找到是哪个对象的m_B属性 cout <<"static func"<<endl; } private: static void funcStatic2(){ // 静态成员函数也具有访问权限 cout <<"static func"<<endl; } }; int Person::m_A = 0; // 静态成员变量:类内声明 类外初始化 void func(){ // 静态成员函数的调用,两种方式:用对象调用,或者用类去调用 Person p; p.funcStatic(); Person::funcStatic(); // Person::funcStatic2(); // 报错:函数 "Person::funcStatic2" (已声明 所在行数:17) 不可访问 // 类外无法访问私有 静态成员函数 } int main(){ func(); system("pause"); return 0; }
静态成员变量必须类内声明,类外初始化的意义:
为了确保静态成员变量只有一个定义,并且可以在类外进行赋值。
当静态成员变量在类内声明时,它只是一个声明而不是定义。这意味着它没有分配内存空间,只有在类外进行初始化时才会分配内存。如果将静态成员变量的初始化放在类内部,每个包含该头文件的源文件都会创建一个独立的静态成员变量,这就会导致重复定义的错误。
通过将静态成员变量的初始化放在类外部,可以确保只有一个实际的定义,并且该变量可以在类外部的任何地方进行赋值。这样做的目的是为了方便对静态成员变量的使用和管理。类外初始化使得静态成员变量在类加载之前就可以被初始化,而不需要实例化类对象。
4.3 C++ 对象模型 和 this 指针
4.3.1 成员变量和成员函数分开存储
C++ 中,类的成员变量和成员函数分开存储
类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中,但对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。
不同对象的成员变量的值可能不同,需要单独分配内存来存储。但是不同对象的成员函数的代码是一样的,若每个对象的内存模型中都保存一份相同的代码片段,则会浪费空间,考虑可以将这些代码片段压缩成一份。
事实上编译器也是这样做的,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存, 但是所有对象都共享同一段函数代码。如图所示,成员变量在堆区或栈区分配内存,成员函数在代码区分配内存。
![](https://img-blog.csdnimg.cn/29da78557bb948819a52080b9e74d039.png)
-
空对象的大小是 1字节,因为C++编译器会给每个空对象也分配一个字节的内存空间,是为了区分空对象占内存的位置,两个空对象不能占用同一个内存,每个控件对象也因该有一个独一无二的内存地址
#include <iostream> using namespace std; class Person{ }; void func(){ Person p; cout <<"sizeof p: "<< sizeof(p) << endl; } int main(){ func(); system("pause"); return 0; }
-
只有非静态成员变量才属于类对象的内存空间中
- 静态变量不占用对象的内存空间
-
普通成员变量占用对象的内存空间
-
普通成员函数,静态成员函数,都不属于类的对象上
4.3.2 this 指针概念
已知C++中成员变量和成员函数是分开存储的,
每一个非静态成员函数 只会有一份函数实例,也就是说多个同类型的对象会公用一块代码区
问题:这一块代码区 是如何区分是那个对象调用自己?
C++ 通过提供特殊的对象指针,this指针,解决该问题,this指针指向被调用的成员函数所属对象
this 指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针到底是什么?
- this 指针实际上是成员函数的一个形参,在调用成员函数时 编译器在编译阶段将对象的地址作为实参传递给 this。 this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值
![]()
- 成员函数最终会被编译成与对象无关的普通函数,除了 成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以 此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁
this指针的用途:
-
当形参和成员变量同名时,可以用this指针进行区分
this指针
-
在类的非静态成员函数中返回对象本身,可以使用
return *this
链式 编程思想,类似于
cout<<
无线追加#include <iostream> using namespace std; class Person{ public: int age; // 引用的方式接收 当前对象的本体 // 值得方式接收 ,接收的是新数据的拷贝返回值,不再是本体 Person AddPersonAge(Person p){ age += p.age; return *this; } }; int main(){ Person p1, p2; p1.age = 10, p2.age = 10; p1.AddPersonAge(p2).AddPersonAge(p2).AddPersonAge(p2); cout << p1.age << endl; system("pause"); return 0; }
-
引用的方式 接收,接收的是本体
-
值传递方式 接收,接收的不是本体
-
4.3.3 空指针访问成员函数
C++ 空指针也是可以调用成员函数的,但是需要注意有没有用到this指针
如果用到this指针,需要加以判断,保证代码的健壮性
#include <iostream>
using namespace std;
class Person{
public:
int m_age;
void showClassName(){
cout << "showClassName" << endl;
}
void showAge(){
if(this == nullptr) return; // 确定this指针不为空
cout <<"age: " << m_age << endl; // 会默认加上一个this->m_age
}
};
int main(){
Person *p = nullptr;
p->showClassName(); // 不会报错
p->showAge();
// 报错,因为p为空指针,nullptr->m_age 无中生有
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/5f927654486d449da2a708ab9fdf43d5.png)
4.3.4 const修饰成员函数
常函数:
- 成员函数后,加const,我们称为该函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时,加关键字mutable后,在常函数中依然可以修改
常对象
- 声明对象前,加const称该对象为常对象
- 常对象只能调用常函数
常函数:
#include <iostream>
using namespace std;
class Person{
public:
int m_A;
mutable int m_B;
void showPerson(){
this->m_A = 100;
//this = nullptr; //报错 提示:分配到“this”(记时错误)
// Person *const this; this是指针常量 不可以修改this的朝向
}
// 成员函数后面加上const,修饰的是this指针,表示this指向的数据的值不可再被修改
void showPerson1() const{
//this->m_A = 100; // 报错 提示:表达式必须是可修改的左值
this->m_B = 10;
// const Person *const this; this朝向不能改,里面的数据也不可以修改
}
};
int main(){
Person p;
p.showPerson();
p.showPerson1();
system("pause");
return 0;
}
-
常函数的const是加在this指针上,保证在函数中,this指针的指向数据也无法被修改
-
可以使用
mutable
关键字,修饰类的成员变量。当成员变量被声明为mutable
时,它可以在const
成员函数中被修改。常对象:
#include <iostream> using namespace std; class Person{ public: int m_A; mutable int m_B; Person(int a, int b):m_A(a),m_B(b){}; void f1(){} void f2() const{}; }; int main(){ const Person p(10,20);// 在对象前加const为常对象,不能修改该对象属性的值 //p.m_A = 100; // 报错 p.m_B = 100; // m_B是特殊值 被关键字mutable修饰,在常对象下也可以修改 // 常对象只能调用常成员函数 //p.f1(); // 报错,调用 普通成员函数,因为普通成员函数可能会将属性修改 p.f2(); // 调用 常成员函数 system("pause"); return 0; }
4.4 友元
好朋友之间的友好关系用friend
关键字表示
借助友元可以访问与其有好友关系的类中的所有成员,包括:public,protected, private属性。
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
在类中使用friend关键字声明全局函数,则全局函数可以访问类的私有成员
#include <iostream>
using namespace std;
class House{
//MyFriend 友元全局函数 可以访问House的私有成员
friend void MyFriend(House *h); // 只要写在类里就可以,不要求写在public
public:
string m_livingRoom; // 公有成员变量
House(){
m_livingRoom = "客厅";
m_bedRoom = "卧室";
}
private:
string m_bedRoom; //私有成员变量
};
// 全局函数 通过在类中使用friend关键字声明该函数,MyFriend()可以访问类的私有成员变量
void MyFriend(House *h){
cout <<"MyFriend is visiting " << h->m_livingRoom << endl;
cout <<"MyFriend is visiting " << h->m_bedRoom << endl;
}
void func(){
House h;
MyFriend(&h);
}
int main(){
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/6d5c7d3b067441a28bac16d12ee4877b.png)
4.4.2 类做友元
friend class 友元类的类名;
让该类作为另一个类的友元类,可以访问另一个类的私有成员
#include <iostream>
using namespace std;
class House{
friend class MyFriend;
public:
House();
string m_livingRoom;
private:
string m_bedRoom;
};
House::House(){
m_livingRoom = "客厅";
m_bedRoom = "卧室";
}
class MyFriend{
public:
MyFriend();
void visit();
House *h;
};
MyFriend::MyFriend(){
h = new House();
}
void MyFriend::visit(){
cout << "MyFriend is visiting " << h->m_livingRoom << endl;
cout << "MyFriend is visiting " << h->m_bedRoom << endl;
// 通过friend声明修饰了本类,本类可以访问House类的私有成员变量
}
int main(){
MyFriend f;
f.visit();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/7360db025e3f4c878a52c1d89c3c3510.png)
除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。
4.4.3 成员函数做友元
friend void MyFriend::visit1();
#include <iostream>
using namespace std;
class House;
class MyFriend{
public:
MyFriend();
void visit1();
void visit2();
private:
House *h;
};
MyFriend::MyFriend(){
h = new House();
}
class House{
//告诉编译器MyFriend类下的visit1()成员函数,作为本来的友元函数,可以访问私有成员
friend void MyFriend::visit1();
public:
House();
string m_livingRoom;
private:
string m_bedRoom;
};
House::House(){
m_livingRoom = "客厅";
m_bedRoom = "卧室";
}
void MyFriend::visit1(){ // 让visit1函数可以访问House的私有成员
cout << "MyFriend is visiting " << h->m_livingRoom << endl;
cout << "MyFriend is visiting " << h->m_bedRoom << endl;
// 通过friend声明修饰了本类,本类可以访问House类的私有成员变量
}
void MyFriend::visit2(){ // 让visit2函数访问不到House中的私有成员
cout << "MyFriend is visiting " << h->m_livingRoom << endl;
//cout << "MyFriend is visiting " << h->m_bedRoom << endl; // 报错
}
int main(){
MyFriend f;
f.visit1();
f.visit2();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/12f4e0249da3405998c52059cdb7919c.png)
4.5 运算符重载
对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
例如两个自定义类型的加减乘除
例如 C++中 使用了运算符重载的地方:
+
号可以对不同类型(int
、float
等)的数据进行加法操作<<
既是位移运算符,又可以配合cout
向控制台输出数据
operator是关键字,专门用于定义重载运算符的函数,可以将operator 运算符名称
看作函数名
4.5.1 加号运算符重载 + - * /
作用:实现两个自定义数据类型相加的运算
通过成员函数 重载+
号 或者 通过全局函数 重载+
号
#include <iostream>
using namespace std;
class Person{
public:
int m_A, m_B;
// 1. 成员函数 重载 + 号
// Person operator+(Person &p){
// Person tmp;
// tmp.m_A = this->m_A + p.m_A;
// tmp.m_B = this->m_B + p.m_B;
// return tmp;
// }
};
// 2. 全局函数 重载 + 号
Person operator+(Person &p1, Person &p2){
Person tmp;
tmp.m_A = p1.m_A + p2.m_A;
tmp.m_B = p1.m_B + p2.m_B;
return tmp;
}
Person operator+(Person &p1, int num){
Person tmp;
tmp.m_A = p1.m_A + num;
tmp.m_B = p1.m_B + num;
return tmp;
}
void func(){
Person p1, p2;
p1.m_A = 10, p1.m_B = 20;
p2.m_A = 20, p2.m_B = 10;
// 成员函数本质调用:Person p3 = p1.operator+(p2);
// 全局函数本质调用:Person p3 = operator+(p1, p2);
Person p3 = p1 + p2;
cout << p3.m_A << ", " << p3.m_B << endl;
//如果没有实现运算符重载,则报错:没有与这些操作数匹配的 "+" 运算符
// 运算符重载 也可以进一步发生函数重载
Person p4 = p1 + 10;
// 如果没有实现运算符重载,则报错:没有与这些操作数匹配的 "+" 运算符
cout << p4.m_A << ", " << p4.m_B << endl; //Person + int
}
int main(){
func();
system("pause");
return 0;
}
注意:
对于内置的数据类型,表达式的运算符是不可以改变的
不要滥用运算符重载 (例如:operator + 中 做减法 )
4.5.2 左移运算符重载 << 输出
作用:可以输出自定义的类型
目的:直接输出对象信息
cout << p << endl;
注意事项:
成员函数无法重载<<运算符,全局函数重载可以做到
cout 的 类型是ostream,需要用引用的方式 保证只有一个 不用副本,需要用链式思想
如果是属性是私有的,则需要把全局运算符重载函数设置为友元函数
#include <iostream>
using namespace std;
class Person{
friend ostream& operator <<(ostream &cout , Person &p);
private:
int m_A, m_B;
public:
Person(int a, int b):m_A(a),m_B(b){}
// // 1. 利用成员函数 重载, 不会利用成员函数重载<<运算符,因为无法实现cout在左侧
// void operator<< (){ // 实现 cout << p;
// }
};
// 2. << 只能利用 全局函数重载 左移运算符
ostream& operator <<(ostream &cout , Person &p){
cout <<"m_A = "<< p.m_A << ", m_B = " << p.m_B;
// 如果还想追加 <<, 则需要保持链式法则,返回一个cout类型
return cout;
}
int main(){
Person p1(10,20), p2(20,10);
cout << p1 << endl;// 在左边第一个<<处报错:没有与这些操作数匹配的 "<<" 运算符
cout << p2 << endl;
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/dd7250f280a54fb884258d5e3dcc22a5.png)
4.5.3 自增 自减运算符重载 a++,++a,a–,–a
作用:通过重载递增运算符,实现自己的整形数据
![](https://img-blog.csdnimg.cn/817e2473394b46dda82c40322622f4d7.png)
注意:
前置++运算符:返回引用,先增加后返回
后置++运算符(占位符):返回值,先记录当前值,增加属性值,再返回记录的值
#include <iostream>
using namespace std;
class MyInteger{
friend ostream& operator<< (ostream &cout, const MyInteger &mint);
public:
MyInteger(int a):m_num(a){}
//重载++前置运算符: 先自增,再返回表达式的值
MyInteger& operator ++(){// 返回引用,是因为可以++(++a) 可以对同一个对象进行操作
m_num++; // 先把属性的值+1
return *this; // 把自身作为一个返回
}
//重载后置++运算符: 先返回表达式(不能直接return,先记录当时结果,最后将记录结果做返回),再自增
MyInteger operator ++(int){ // int代表占位参数,可以用于区分前置和后置递增 只认int为后置递增
MyInteger tmp = *this;
m_num++;
return tmp;// 局部对象tmp函数结束后被释放掉,所以返回值,不能返回引用(非法操作)
}
// 前置递减运算符: 先自减,再返回
MyInteger& operator -- (){
m_num--;
return *this;
}
// 后置递减运算符 占位符:先记录当前值,自减,返回记录值
MyInteger operator--(int){
MyInteger tmp = *this;
m_num--;
return tmp;
}
private:
int m_num;
};
// 重载 << 运算符
ostream& operator<< (ostream &cout, const MyInteger &mint){
cout << "mint = " << mint.m_num;
return cout;
}
int main(){
MyInteger mint(10);
cout << mint << endl; // 10
cout << ++mint << endl; // 11
cout << mint++ << endl; // 11
cout << mint << endl; // 12
cout << --mint << endl; // 11
cout << mint-- <<endl; // 11
cout << mint << endl; // 10
// mint++; // 报错:没有与这些操作数匹配的 "++" 运算符
// mint--; // 报错:没有与这些操作数匹配的 "--" 运算符
system("pause");
return 0;
}
结果:
![](https://img-blog.csdnimg.cn/73949080e69543b385a8d4b2ad0c1bf3.png)
4.5.4 赋值运算符重载 =
等号
C++编译器至少给一个类添加4个函数:
-
默认构造函数(无参,函数体为空)
-
默认析构函数(无参,函数体为空)
-
默认拷贝构造函数
-
赋值运算符 operator =,对属性进行值拷贝
-
编译器自己给的 重载赋值运算符是浅拷贝,可能导致堆内存释放的问题
-
#include <iostream> using namespace std; class Person{ public: int *m_Age; public: Person(int a):m_Age(new int(a)){} ~Person(){ if(m_Age != nullptr){ delete m_Age; m_Age = nullptr; } } }; void func(){ Person p1(10); Person p2(20); cout << *p1.m_Age << endl; cout << *p2.m_Age << endl; p2 = p1; // 赋值操作:将p1的所有属性值,赋给p2 cout << *p1.m_Age << endl; cout << *p2.m_Age << endl; } int main(){ func(); system("pause"); return 0; }
-
解决上面的问题:自己重写 赋值运算符重载函数 operator =
,函数体内需利用深拷贝 实现赋值操作
#include <iostream>
using namespace std;
class Person{
public:
int *m_Age;
Person(int age){
m_Age = new int(age);
}
Person& operator =(Person &p){ // 形式p2 = p1
// 0. 编译器提供的是浅拷贝
// m_Age = p.m_Age;
// 1. 先处理自我赋值
if (this == &p) {
return *this;
}
// 2. 再判断是否对象 属性指向堆区,如果有则先释放干净后,然后再进行深拷贝
if(m_Age != nullptr){
delete m_Age;
m_Age = nullptr;
}
m_Age = new int(*p.m_Age); // 深拷贝
return *this; // 返回自身
}
~Person(){
if(m_Age != nullptr){
delete m_Age;
m_Age = nullptr;
}
}
};
ostream& operator<< (ostream &cout, Person &p){
cout << "m_Age = " << *p.m_Age;
return cout;
}
void func(){
Person p1(10), p2(20), p3(30);
cout << p1 << endl << p2 << endl << p3 << endl << endl;
p1 = p2 = p3; // 需要把p3属性赋值给p2,再拿p2属性值赋值给p1
cout << p1 << endl << p2 << endl << p3 << endl << endl;
int a = 10, b = 20, c = 30;
a = b = c; // 连等操作
printf("a=%d, b=%d, c=%d\n",a,b,c); //a=30, b=30, c=30
}
int main(){
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/01e973609501419bac9821ad924d99df.png)
4.5.5 关系运算符重载 !=, ==
作用:可以让两个自定义类型对象进行比较
![](https://img-blog.csdnimg.cn/6acebd6e198749899987bdaf572cc343.png)
#include <iostream>
using namespace std;
class Person{
public:
string m_Name;
int m_Age;
Person(string name, int age):m_Name(name),m_Age(age){}
// 重载 == 运算符
bool operator == (const Person &p){
if(m_Name == p.m_Name && m_Age == p.m_Age) return true;
return false;
}
// 重载 != 运算符
bool operator != (const Person &p){
if(m_Name == p.m_Name && m_Age == p.m_Age) return false;
return true;
}
};
void func(){
Person p1("Rachel",20), p2("Rachel", 20), p3("Tom", 20);
if(p1 == p2) cout << "p1 和 p2 相等" << endl;
else cout << "p1 和 p2 不相等" << endl;
if(p2 == p3) cout << "p2 和 p3 相等" << endl;
else cout << "p2 和 p3 不相等" << endl;
if(p1 != p2) cout << "p1 和 p2 不相等" << endl;
else cout << "p1 和 p2 相等" << endl;
if(p2 != p3) cout << "p2 和 p3 不相等" << endl;
else cout << "p2 和 p3 相等" << endl;
// if(p1 == p2) cout << "p1和p2相等" << endl; // 报错:没有与这些操作数匹配的 "==" 运算符
}
int main(){
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/33b778c2676e40d58bd0a593e837a92c.png)
4.5.6 函数调用运算符重载 ()
- 函数调用运算符() 也可以重载
- 由于重载后的方式非常像函数的调用,因此称为仿函数(STL中有使用该概念)
- 函数没有固定写法,非常灵活
#include <iostream>
using namespace std;
class MyPrint{
public:
void operator() (string test){
cout << test << endl;
}
};
void MyPrint1(string test){
cout << test << endl;
}
void func(){
MyPrint myprint;
myprint("hello world"); // 由于使用非常像一个函数,因此被称为仿函数
MyPrint1("hello world"); // 真正的函数调用
}
/**********仿函数非常灵活,没有固定写法********************
* 例如下面的类的()重载函数 有两个参数 且有返回值
* 而上面的类的()重载函数中,有一个参数 且 没有返回值
* ******************************************************/
class MyAdd{
public:
int operator()(int num1, int num2){
return num1+num2;
}
};
void func1(){
MyAdd myadd;
int ret = myadd(100,100);
cout << ret << endl;
// 匿名对象 这行使用完就释放 生命周期就一行
cout << MyAdd()(200,200) << endl;
}
int main(){
func();
func1();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/a0e710eae37e4b12864beffb1e543905.png)
4.6 继承
继承是面向对象三大特性之一
![](https://img-blog.csdnimg.cn/d282cf2190f64360b29f41288c56cc4a.png)
没有继承的类,可能会导致代码重复:
举例:网页页面
#include <iostream>
using namespace std;
// 普通实现页面: Java页面
class Java{
public:
void header(){
cout <<"首页、登录、注册...(公共头部)" << endl;
}
void footer(){
cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" <<endl;
}
void contect(){
cout <<"Java 学科视频" << endl;
}
};
// 普通实现页面: Python页面
class Python{
public:
void header(){
cout <<"首页、登录、注册...(公共头部)" << endl;
}
void footer(){
cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" <<endl;
}
void contect(){
cout <<"Python 学科视频" << endl;
}
};
// 普通实现页面: C++页面
class CPP{
public:
void header(){
cout <<"首页、登录、注册...(公共头部)" << endl;
}
void footer(){
cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" <<endl;
}
void contect(){
cout <<"C++ 学科视频" << endl;
}
};
void func(){
cout<<"*************************\nJava 页面如下:\n";
Java ja;
ja.header(),ja.footer(),ja.left(),ja.contect();
cout<<"*************************\nPython 页面如下:\n";
Python p;
p.header(),p.footer(),p.left(),p.contect();
CPP c;
cout<<"*************************\nCPP 页面如下:\n";
c.header(),c.footer(),c.left(),c.contect();
}
int main(){
func();
system("pause");
return 0;
}
4.6.1 继承的基本语法
class Base{};
class Java : public Base{};
class Python : public Base{};
class CPP : public Base{};
// 语法:class 子类:继承方式 父类{};
继承实现以上页面,实际开发中 减少重复代码
#include <iostream>
using namespace std;
class Base{
public:
void header(){
cout <<"首页、登录、注册...(公共头部)" << endl;
}
void footer(){
cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
}
void left(){
cout << "Java、Python、C++...(公共分类列表)" <<endl;
}
};
// 普通实现页面: Java页面
class Java : public Base{
public:
void contect(){
cout <<"Java 学科视频" << endl;
}
};
// 普通实现页面: Python页面
class Python : public Base{
public:
void contect(){
cout <<"Python 学科视频" << endl;
}
};
// 普通实现页面: C++页面
class CPP : public Base{
public:
void contect(){
cout <<"C++ 学科视频" << endl;
}
};
void func(){
cout<<"*************************\nJava 页面如下:\n";
Java ja;
ja.header(),ja.footer(),ja.left(),ja.contect();
cout<<"*************************\nPython 页面如下:\n";
Python p;
p.header(),p.footer(),p.left(),p.contect();
CPP c;
cout<<"*************************\nCPP 页面如下:\n";
c.header(),c.footer(),c.left(),c.contect();
}
int main(){
func();
system("pause");
return 0;
}
总结:
- 继承的好处:可以减少重复代码
- 语法:
class A: public B {};
- A类 称为子类 或者派生类
- B类 被称为父类 或者 基类
- 派生类中的成员包含两大部分:一类是从基类继承过来的,一类是自己增加的成员
- 从基类继承过来的表现其共性,而新增的成员体现了其个性
4.6.2 继承方式
继承方式:
-
公共继承
-
保护继承
-
私有继承
#include <iostream>
using namespace std;
class Base{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
// public公有继承
class Son1:public Base{
void fun(){
m_A = 10; // 父类中的public权限成员 到子类中依然是public权限
m_B = 10; // 父类中的protected权限成员 到子类中依然是protected权限 类内访问类外无法访问
//m_C = 10; // 父类中的private权限成员 子类无法访问
}
};
void func1(){ // 类外访问Son1的属性
Son1 s1;
s1.m_A = 10;
//s1.m_B = 10; // 在Son1中,m_B是protected权限
//s1.m_C = 10; // private
}
class Son2:protected Base{
void fun(){
m_A = 10; // 父类中public成员,到子类中变为protected权限
m_B = 10; // 父类中protected成员,到子类中变为protected权限
m_C = 10; // 子类访问不到父类的private私有成员
}
};
void func2(){ // 类外访问Son1的属性
Son2 s2;
//s2.m_A = 10; // 在Son2中,m_A变为protected权限,因此类外访问不到
//s2.m_B = 10; // 在Son2中,m_B是protected权限 类外不能访问
//s2.m_C = 10; // 在Son2中,m_C是private权限
}
class Son3:private Base{
void fun(){
m_A = 10; // 父类中public成员,到子类中变为private权限
m_B = 10; // 父类中protected成员,到子类中变为private权限
//m_C = 10; // 子类访问不到父类的private私有成员
}
};
void func3(){ // 类外访问Son1的属性
Son3 s3;
//s3.m_A = 10; // 在Son3中,m_A变为private权限,因此类外访问不到
//s3.m_B = 10; // 在Son3中,m_B变为private权限,因此类外访问不到
//s3.m_C = 10; // 在Son3中,m_C是private权限,类外访问不到
}
class GrandSon3:public Son3{ // 公共继承Son3 也无法使用其属性,证明儿子也访问不到,是private
void func(){
//m_A = 10; // 子类访问不到父类的private私有成员
//m_B = 10; // 子类访问不到父类的private私有成员
//m_C = 10; // 子类访问不到父类的private私有成员
}
};
int main(){
func1();
func2();
func3();
system("pause");
return 0;
}
4.6.3 继承中的对象内存模型
-
复习:没有继承时对象内存的分布情况。
成员变量和成员函数分开存储
- 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象)
- 成员函数与对象内存分离,存储在代码区
问题:从父类继承过来的成员,哪些属于子类对象中?
-
父类中所有非静态成员变量都会被子类继承下去,父类中的私有成员属性 是被编译器隐藏了,因此子类访问不到,但是确实是继承了。
-
所有成员函数仍然存储 在另外一个区域——代码区,由所有对象共享
举例1:父类中含有不同权限的属性,子类继承后 子类的内存大小是多少? 16字节 (父类所有 + 子类所有)
![](https://img-blog.csdnimg.cn/638972bd79f445dab89c2096da7b0b81.png)
举例2:父类和子类中出现成员变量的遮蔽,即父类成员变量和子类成员变量同名,则子类内存大小? 不变
![](https://img-blog.csdnimg.cn/699aa3cb3b3f4b4dbdfd418ee2ae9a38.png)
4.6.4 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类 和 子类的 构造、析构顺序 是谁先谁后?
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
#include <iostream>
using namespace std;
class Base {
public:
Base(){
cout << "Base构造函数!" << endl;
}
~Base(){
cout << "Base析构函数!" << endl;
}
};
class Son : public Base{
public:
Son(){
cout << "Son构造函数!" << endl;
}
~Son(){
cout << "Son析构函数!" << endl;
}
};
void func(){
//继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
Son s;
}
int main() {
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/2cc8a669c5854d34b31d93cc016ee637.png)
4.6.5 继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
举例:
#include <iostream>
using namespace std;
class Base {
public:
int m_A;
Base():m_A(100){} // 构造函数中给属性m_A赋值100
void prints(){
cout <<"Base prints()"<<endl;
}
void prints(int a){
cout <<"Base prints(int a)"<<endl;
}
};
class Son : public Base{
public:
int m_A;
Son():m_A(200){} // 构造函数中给属性m_A赋值200
void prints(){
cout <<"Son"<<endl;
}
};
void func(){
/*同名属性的处理方式:子类对象访问父类的同名成员变量,需要加上父类的作用域*/
Son s; //子类中将会有两个m_A,一个来自父类(100),一个来自子类(200)
cout << s.m_A << endl; // 200
cout << s.Base::m_A << endl; // 100
/*同名属性的处理方式:和属性一样*/
s.prints();
s.Base::prints();
// s.prints(100); //出错,但删除子类同名函数后不报错,被隐藏 必须加上作用域
s.Base::prints(100);
/*
如果子类中出现和父类同名的成员函数,
子类的同名成员函数会隐藏掉父类所有的同名成员函数(所有重载都不能直接访问)
*/
}
int main() {
func();
system("pause");
return 0;
}
总结:
- 子类对象可以直接访问到子类的同名成员
- 子类对象加上父类的作用域可以访问到父类的同名成员
- 当子类和父类拥有同名的成员函数,子类会隐藏父类中所有的同名成员函数,必须加上父类的作用域才可以访问到父类中的同名函数
4.6.6 继承同名静态成员处理方式
复习:静态成员变量和静态成员函数的特点
静态成员变量特点:所有对象共享同一片数据,编译期在全局/静态数据区分配内存,类内声明类外初始化
静态成员函数特点:函数中只能访问静态成员变量
问题:继承中 同名的静态成员在子类对象中如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
#include <iostream>
using namespace std;
class Base {
public:
static int m_A;
static void f(){
cout << "Base f()" << endl;
}
static void f(int a){
cout <<"Base f(int a)" << endl;
}
};
int Base::m_A = 200; // 静态成员变量,必须类内声明,类外初始化
class Son : public Base{
public:
static int m_A;
static void f(){
cout << "Son f()" << endl;
}
};
int Son::m_A = 200; //Son会继承Base类的静态成员变量,因此Son作用于下有两个静态成员变量 m_A, Base::m_A
void func(){
/*******************同名静态成员变量*******************/
// 1.通过对象访问静态成员变量
Son s;
cout << s.m_A << endl;
cout << s.Base::m_A << endl;
// 2.通过类名访问静态成员变量
cout << Son::m_A << endl;
cout << Son::Base::m_A << endl;
//:: 第一个表示以类名方式访问; 第二个表示访问父类作用域下的
/*******************同名静态成员函数*******************/
// 1.通过对象访问静态成员函数
s.f();
s.Base::f();
// 2.通过类名访问静态成员函数
Son::f();
Son::Base::f();
// Son::f(10);
// 报错:和非静态成员函数一样,子类会把父类所有同名函数隐藏掉 包括重载函数,如果使用需加上父类作用域
Son::Base::f(10);
}
int main() {
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/5226a83996ff432f8ce3bd12abc70f5f.png)
总结:同名静态成员处理方式和非静态成员处理方式一样,只不过有两种访问方式(通过对象 和 通过类名)
4.6.7 多继承语法
C++ 允许一个类继承多个类
语法:class 子类:继承方式 父类1, 继承方式 父类2 ... { };
多继承的问题:若父类1和父类2中有同名成员出现,子类使用时不知道是哪一个,因此需加父类作用域加以区分
举例:
#include <iostream>
using namespace std;
class Base1 {
public:
Base1():m_A(100){ }
public:
int m_A;
};
class Base2 {
public:
Base2():m_A(200){ }
public:
int m_A;
};
//语法:class 子类:继承方式 父类1 ,继承方式 父类2
class Son : public Base2, public Base1 {
public:
Son():m_C(300),m_D(400){ }
public:
int m_C;
int m_D;
};
//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
int main() {
Son s;
cout << "sizeof Son = " << sizeof(s) << endl; //16字节
// cout << s.m_A << endl; // 报错,"Son::m_A" 不明确, 需要显示给出作用域
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
system("pause");
return 0;
}
4.6.8 菱形继承
菱形继承概念: 两个派生类B,C继承同一个基类A,又有某个类D同时继承 上面的两个派生类。这种继承被称为菱形继承,或者钻石继承
![](https://img-blog.csdnimg.cn/6778c5496bfe485fade6ce1e29f2493e.png)
典型的案例:
![](https://img-blog.csdnimg.cn/19f4d69a27bc4957b0bacd40d1c16ba3.png)
-
没有虚继承
-
#include <iostream> using namespace std; class Animal{ public: int m_Age; }; class Sheep : public Animal {}; class Tuo : public Animal {}; class SheepTuo : public Sheep, public Tuo {}; void func() { SheepTuo st; //st.m_Age = 18; //报错:"SheepTuo::m_Age" 不明确,出现二义性 // 菱形继承,两个父类拥有相同的数据,需要加作用域 以区分 st.Sheep::m_Age = 18; st.Tuo::m_Age = 28; // 问题:数据m_Age只要一份即可,菱形继承导致数据有两份,只要一份即可,资源浪费 }
-
-
m_Age 只需要一份即可,使用虚继承后:
-
#include <iostream> using namespace std; class Animal{ public: int m_Age; }; //继承前加 virtual 关键字后,变为虚继承 //此时公共的父类Animal称为虚基类 class Sheep : virtual public Animal {}; class Tuo : virtual public Animal {}; class SheepTuo : public Sheep, public Tuo {}; void func(){ SheepTuo st; // 菱形继承,两个父类拥有相同的数据,需要加作用域 以区分 st.Sheep::m_Age = 18; st.Tuo::m_Age = 28; // 问题:数据m_Age只要一份即可 菱形继承导致数据有两份,只要一份即可 资源浪费 -> 利用虚继承,解决问题 cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; // 28 只有一份 cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; // 28 cout << "st.m_Age = " << st.m_Age << endl; // 28 m_Age只有一份,不会出错 cout << "sizeof: " << sizeof(st) << endl; } int main() { func(); system("pause"); return 0; }
-
-
4.7 多态
多态是C++面向对象三大特性之一
4.7.1 多态的基本概念
多态分为两类:
- 静态多态:函数重载 和 运算符重载 属于静态多态,复用函数名
- 动态多态:派生类 和 虚函数 实现运行时多态
函数在内存中是一个代码段,函数名是进入该代码段的入口。在编译期间将函数名换为代码段入口,称为函数绑定
- 静态多态的函数早绑定 —— 编译阶段 确定函数地址
- 绑定的是静态类型,在编译器就可以确定函数或者对象的类型
- 静态类型:对象在声明时 采用的类型,在编译期确定
- 动态多态的函数玩绑定 —— 运行阶段 确定函数地址
- 绑定的是动态类型,只有在运行时才可以确定函数或者对象的类型
- 动态类型:一个指针或者引用目前的对象类型,只有运行时才能确定。
#include <iostream>
using namespace std;
class Animal{
public:
// Speak函数就是虚函数
// 函数前面加上virtual关键字,变成虚函数,那么编译器在编译期 无法确定函数地址
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal{
public:
void speak()
{
cout << "小猫: 喵" << endl;
}
};
class Dog :public Animal{
public:
void speak(){
cout << "小狗: 汪" << endl;
}
};
/*我们希望传入什么对象,那么就调用什么对象的函数
如果函数地址在编译阶段就能确定,那么静态编译
如果函数地址在运行阶段才能确定,就是动态编译 */
void DoSpeak(Animal & animal){
animal.speak();
}
/*
多态满足条件:
1、有继承关系
2、子类重写父类中的虚函数
多态使用: 父类指针或引用指向子类对象
*/
int main() {
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/4a4d8f559e754805a49635511c68fd74.png)
总结:
- 多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
- 多态使用条件:父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就 使用派生类的成员。即基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有 多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
多态底层原理:虚函数(表)指针,虚函数表
-
Animal类 函数不加virtual关键字的 类大小:1字节,空类1字节的"区分占位符"
-
Animal类 函数加上 virtual 关键字的 类大小:8字节,是虚表指针的大小 64位8字节,32位4字节
-
Cat类不写 speak() 函数,继承Animal类的虚函数指针,指向Animal的speak()函数
-
Cat类 重写speak() 函数:子类Cat类将自己的虚函数表中的函数地址替换成自己重写的speak()函数。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那 么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。
当通过指针调用虚函数时,现根据指针找到子类对象的vfptr,再根据vfptr找到子类的虚函数入口地址,进而获取到子类重写虚函数的函数地址
-
-
哪些函数不能是虚函数?
构造函数,静态成员函数(不属于类),内联函数(编译器替换),友元函数,普通函数
-
构造函数:
- 意义上 构造函数初始化派生类对象,派生类必须知道基类函数的初始化定义 才能构造
- 实现上,构造函数初始化时 会初始化虚表指针,指向虚函数。若构造函数是虚函数,则需要通过虚表指针查找虚表 找对应虚函数地址,但此时虚表指针还没有被初始化,因此也无法实现,编译器会报错
-
静态函数:静态成员函数 不属于对象属于类,静态成员函数没有this指针,没有意义
-
内联函数:内联函数是编译器进行替换工作,虚函数在运行期间才确定类型,因此内联函数不能是虚函数
-
友元函数:友元函数不属于类的成员函数,不能被继承
-
普通函数:不同函数不属于类的成员函数,不能被继承,没有继承特性的函数没有虚函数的说法
4.7.2 多态案例一:计算器类(两种实现方式:普通类,多态实现)
案例描述:
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
示例:
#include <iostream>
using namespace std;
//普通实现
class Calculator {
public:
double getResult(string oper){
if (oper == "+") {
return m_Num1 + m_Num2;
}
else if (oper == "-") {
return m_Num1 - m_Num2;
}
else if (oper == "*") {
return m_Num1 * m_Num2;
}
//如果要提供新的运算,需要修改源码
return m_Num1 / m_Num2;
}
public:
double m_Num1;
double m_Num2;
};
void func1(){
//普通实现测试
cout << "测试 普通类实现的计算器:" << endl;
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}
//多态实现,利用抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class BaseCalculator{
public :
virtual double getResult(){
return 0;
}
double m_Num1;
double m_Num2;
};
//加法计算器
class AddCalculator :public BaseCalculator{
public:
double getResult(){
return m_Num1 + m_Num2;
}
};
//减法计算器
class SubCalculator :public BaseCalculator{
public:
double getResult(){
return m_Num1 - m_Num2;
}
};
//乘法计算器
class MulCalculator :public BaseCalculator{
public:
double getResult(){
return m_Num1 * m_Num2;
}
};
void func2(){
cout << "测试 多态实现的计算器:" << endl;
//创建加法计算器
BaseCalculator *cal = new AddCalculator;
cal->m_Num1 = 10;
cal->m_Num2 = 10;
cout << cal->m_Num1 << " + " << cal->m_Num2 << " = " << cal->getResult() << endl;
delete cal; //用完销毁 释放申请的堆内存空间
//创建减法计算器
cal = new SubCalculator;
cal->m_Num1 = 10;
cal->m_Num2 = 10;
cout << cal->m_Num1 << " - " << cal->m_Num2 << " = " << cal->getResult() << endl;
delete cal;
//创建乘法计算器
cal = new MulCalculator;
cal->m_Num1 = 10;
cal->m_Num2 = 10;
cout << cal->m_Num1 << " * " << cal->m_Num2 << " = " << cal->getResult() << endl;
delete cal;
}
int main() {
func1();
func2();
system("pause");
return 0;
}
总结:C++开发提倡利用多态设计程序架构,因为多态优点很多
4.7.3 纯虚函数 和 抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
#include <iostream>
using namespace std;
/*
抽象类特点:
1. 无法实例化对象
2. 子类必须重写父类纯虚函数,否则子类也是抽象类
*/
class Base{
public:
virtual void func() = 0; //纯虚函数:虚函数virtual关键字的基础上,在函数后面 = 0
};
class Son : public Base{
public:
void func(){
cout <<"Son func()" <<endl;
}
};
int main() {
/* 尝试实例化抽象类 */
//Base b; //报错:不允许使用抽象类类型 "Base" 的对象, 函数 "Base::func" 是纯虚拟函数
//new Base; // 报错同上,无论是栈上实例化 还是堆区实例化,都无法实例化具有纯虚函数的抽象类
/* 如果子类不重写纯虚函数 子类也是抽象类*/
// Son s;// 报错:不允许使用抽象类类型 "Son" 的对象, 纯虚拟 函数 "Base::func" 没有强制替代项
/* 如果子类重写了纯虚函数 可以实例化对象*/
Son s; // 没问题,子类必须重写纯虚函数,否则无法实例化对象
Base *b = new Son;// 使用多态调用func()函数
b->func();
delete b;// 销毁
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/4fc81d352de14de4b8aca1b1476e67f9.png)
4.7.4 多态案例二:制作饮品
案例描述:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
#include <iostream>
using namespace std;
class AbstractMakeDrink{
public:
//四个步骤:煮水 - 冲泡 - 倒入杯中 - 加入辅料
virtual void boilingWater() = 0;
virtual void brewing() = 0;
virtual void pouring() = 0;
virtual void addingIngredients() = 0;
void makeDrink(){
boilingWater();
brewing();
pouring();
addingIngredients();
}
};
class MakeCoffee : public AbstractMakeDrink{
void boilingWater(){ printf("煮水\n"); }
void brewing(){ printf("冲咖啡\n"); }
void pouring(){ printf("倒入咖啡杯\n"); }
void addingIngredients(){ printf("加入牛奶\n"); }
};
class MakeTea : public AbstractMakeDrink{
void boilingWater(){ printf("煮矿泉水\n"); }
void brewing(){ printf("泡茶\n"); }
void pouring(){ printf("倒入茶壶\n"); }
void addingIngredients(){ printf("加入桂花\n"); }
};
void doWork(AbstractMakeDrink *make){
make->makeDrink();
delete make;
cout << "--------------" << endl;
}
int main() {
doWork(new MakeCoffee);
doWork(new MakeTea);
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/49ce65d20dd844ac80596c465d18a9e3.png)
4.7.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
-
案例1:不使用虚析构函数
-
案例2:使用虚析构函数
-
#include <iostream> using namespace std; class Animal { public: Animal(){ cout << "Animal 构造函数" << endl; } virtual void Speak() = 0; // 析构函数加上virtual关键字,变成虚析构函数 virtual ~Animal(){ cout << "Animal析构函数" << endl; } }; class Cat : public Animal { public: Cat(string name):m_Name(new string(name)){ cout << "Cat构造函数" << endl; } virtual void Speak(){ cout << *m_Name << "小猫: 喵" << endl; } ~Cat(){ cout << "Cat析构函数" << endl; if (this->m_Name != NULL) { delete m_Name; m_Name = NULL; } } public: string *m_Name; }; void func(){ Animal *animal = new Cat("Tom"); animal->Speak(); //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏 //怎么解决?给基类增加一个虚析构函数 //虚析构函数就是用来解决通过父类指针释放子类对象 delete animal; } int main() { func(); system("pause"); return 0; }
-
-
-
案例3:使用纯虚析构函数
-
#include <iostream> using namespace std; class Animal { public: Animal(){ cout << "Animal 构造函数" << endl; } virtual void Speak() = 0; virtual ~Animal() = 0; }; /* 无论是虚析构函数 还是 纯虚析构函数,目的都是为了父类指针释放子类对象 区别: 纯虚析构函数需要声明 也需要有实现,因为父类可能也有堆内存需要释放 纯虚析构 该类也是抽象类,无法被实例化 */ Animal::~Animal() { cout << "Animal 纯虚析构函数调用!" << endl; } class Cat : public Animal { public: Cat(string name):m_Name(new string(name)){ cout << "Cat构造函数" << endl; } virtual void Speak(){ cout << *m_Name << "小猫: 喵" << endl; } ~Cat(){ cout << "Cat析构函数" << endl; if (this->m_Name != NULL) { delete m_Name; m_Name = NULL; } } public: string *m_Name; }; void func(){ Animal *animal = new Cat("Tom"); animal->Speak(); delete animal; } int main() { func(); system("pause"); return 0; }
-
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 纯虚析构函数需要声明与实现(因为父类对象也可能有堆内存需要释放),拥有纯虚析构函数的类也属于抽象类,无法被实例化
4.7.6 多态案例二:电脑组装
案例描述:
电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作
#include <iostream>
using namespace std;
class CPU{
public:
virtual void calculate() = 0;
};
class VideoCard{
public:
virtual void display() = 0;
};
class Memory{
public:
virtual void storage() = 0;
};
class Computer{
public:
Computer(CPU *c, VideoCard *v,Memory *m):m_CPU(c),m_VideoCard(v),m_Memory(m){ }
void work(){
cout << "组装电脑,配置如下:" <<endl;
m_CPU->calculate();
m_VideoCard->display();
m_Memory->storage();
}
~Computer(){
if(m_CPU != nullptr){
delete m_CPU;
m_CPU = nullptr;
}
if(m_VideoCard != nullptr){
delete m_VideoCard;
m_VideoCard = nullptr;
}
if(m_Memory != nullptr){
delete m_Memory;
m_Memory = nullptr;
}
}
private:
CPU *m_CPU;
VideoCard *m_VideoCard;
Memory *m_Memory;
};
/*********************不同品牌的CPU 显卡 内存条的实现*********************/
class IntelCPU : public CPU{
public:
void calculate(){
cout <<"InterCPU calculate()" <<endl;
}
};
class IntelVideoCard : public VideoCard{
public:
void display(){
cout <<"InterVideoCard display()" <<endl;
}
};
class IntelMemory : public Memory{
public:
void storage(){
cout <<"InterMemory storage()" <<endl;
}
};
class LenovoCPU : public CPU{
public:
void calculate(){
cout <<"LenovoCPU calculate()" <<endl;
}
};
class LenovoVideoCard : public VideoCard{
public:
void display(){
cout <<"LenovoVideoCard display()" <<endl;
}
};
class LenovoMemory : public Memory{
public:
void storage(){
cout <<"LenovoMemory storage()" <<endl;
}
};
void func1(){
CPU *intelCPU = new IntelCPU();
VideoCard *intelCard = new IntelVideoCard();
Memory *intelMem = new IntelMemory();
Computer *computer1 = new Computer(intelCPU,intelCard,intelMem);
computer1->work();
delete computer1;
cout <<"---------------------------------"<<endl;
Computer *computer2 =
new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
computer2->work();
delete computer2;
cout <<"---------------------------------"<<endl;
Computer *computer3 = new Computer
(new IntelCPU, new LenovoVideoCard, new IntelMemory);
computer2->work();
delete computer3;
}
int main() {
func1();
system("pause");
return 0;
}
自己要注意的点:
哪里创建,哪里就析构 -> Computer类
不要在func1() 中逐个delete intelCPU指针,intelCard指针,intelMem指针
而是在析构函数中 判断指针是否为空,然后在析构函数中释放指针所指向的内存空间
使用析构函数 思维 还是不太熟练
5 文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件 <fstream>
文件类型分为两种:
- 文本文件 - 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作 output CPU向内存中写
- ifstream: 读操作 input CPU把数据从内存读向CPU
- fstream : 读写操作 file
5.1文本文件
5.1.1写文件
写文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ofstream ofs;
-
打开文件
ofs.open("文件路径",打开方式);
-
写数据
ofs << "写入的数据";
类似于
cout << "输出到显示屏的数据";
只不过一个输出到显示屏,一个输出到内存文件 -
关闭文件
ofs.close();
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意: 文件打开方式可以配合使用,利用|操作符
**例如:**用二进制方式写文件 ios::binary | ios:: out
,类似 Linux中的文件权限 也是通过| 组合不同的权限
示例:
#include <iostream>
#include <fstream> // 1.包含头文件fstream
using namespace std;
void func()
{
ofstream ofs; // 2.创建 输出流/写流对象
ofs.open("test.txt", ios::out); // 3.打开文件text.txt ,打开方式out 为写文件打开文件
// 4.用左移运算符 写入内容
ofs << "姓名:小红" << endl;
ofs << "性别:女" << endl;
ofs << "年龄:9" << endl;
// 5. 关闭文件
ofs.close();
}
int main() {
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/33921cf4edc9443ea59be05c3ae8104e.png)
总结:
- 文件操作必须包含头文件
fstream
- 写文件 可以利用
ofstream
,或者fstream
类- 记得创建对应流的对象
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用
<<
可以向文件中写数据- 操作完毕,要关闭文件
对象名.close()
5.1.2读文件
读文件与写文件步骤相似,但是读取方式相对于比较多
读文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ifstream ifs;
-
打开文件并判断文件是否打开成功
ifs.open(“文件路径”,打开方式);
-
读数据
四种方式读取
-
关闭文件
ifs.close();
示例:
#include <iostream>
#include <fstream> // 1.包含头文件fstream
#include <string>
using namespace std;
void func(){
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open()){
cout << "文件打开失败" << endl;//路径写错或者文件不存在
return;
}
// // 第一种方式 字符数组全初始化为0,while循环把数据>>右移运算符放到buf中,读不到数据退出
// char buf[1024] = { 0 };
// while (ifs >> buf){
// cout << buf << endl;
// }
// // 第二种 ifstream类提供的方法getline():获取一行 放到buf中 参数1一个字符数组 用于存储数据;参数2 最大要提取的字符数
// char buff[1024] = { 0 };
// while (ifs.getline(buf,sizeof(buf))){
// cout << buff << endl;
// }
// // 第三种 把数据放到C++的字符串中 getline()系统函数 参数1-输入流 参数2-用于存储的缓冲区
// string buffer;
// while (getline(ifs, buffer)){
// cout << buf << endl;
// }
// 第四种 每次只获取一个字符,只要能c不是文件末尾 就一直读
char c;
while ((c = ifs.get()) != EOF){ // end of file,#define EOF (-1)
cout << c;
}
ifs.close();
}
int main() {
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/187c220b471245da8fe0f938974884e5.png)
总结:
- 读文件可以利用 ifstream ,或者fstream类
- 利用is_open函数可以判断文件是否打开成功
- close 关闭文件
5.2 二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为 ios::binary
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型 :ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include <iostream>
#include <fstream> // 1.包含头文件fstream
#include <string>
using namespace std;
class Person{
public:
char m_Name[64];
int m_Age;
};
//二进制文件 写文件
void func()
{
//1、包含头文件
//2、创建输出流对象 也可以用构造函数的 时候打开文件 省略第三步
ofstream ofs("person.txt", ios::out | ios::binary);
//3、打开文件
//ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三" , 18};
//4、写文件 ostream& write(const char * buffer,int len)
ofs.write((const char *)&p, sizeof(p));
//5、关闭文件
ofs.close();
}
int main() {
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/38496b37cc634479a1da6759019787e9.png)
总结:文件输出流对象 可以通过write函数,以二进制方式写数据
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include <iostream>
#include <fstream> // 1.包含头文件fstream
#include <string>
using namespace std;
class Person{
public:
char m_Name[64];
int m_Age;
};
void func(){
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open()){
cout << "文件打开失败" << endl;
}
Person p;
// istream& read(char *buffer,int len);
ifs.read((char *)&p, sizeof(p));
cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}
int main() {
func();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/5c8de8bf778046719279c3f75edf28da.png)
总结:文件输入流对象 可以通过read函数,以二进制方式读数据