《黑马程序员》学习记录
一、C++ 基础编程
1、初识
- 注释:单行注释,多行注释
- 变量
- 常量:
define
宏常量:#define A 10
const
修饰的变量:const int A = 10;
- 关键字
- 标识符命名规则:
- 不能是关键字
- 只能由字母、数字、下划线组成
- 第一个字符不能是数字
- 标识符中字母区分大小写
2、数据类型
- 整型:short(2bytes),int(4bytes),long(win为4bytes,Linux为8bytes),long long(8bytes)
- sizeof 关键字:统计数据类型所占空间的大小
- 实型:浮点型,float(4bytes,7位有效数字),double(8bytes,15~16位有效数字)
默认情况下,C++ 只会输出六位有效数字的小数!!!
- 字符型:char(1bytes),存放的是 ASCII 值;
char a = '1';
- 转义字符
- 字符串:
string a = "1234"
(C++风格),char str[] = "1234"
(C语言风格)
C++ 风格的字符串需要引用头文件
# include <string>
- 布尔类型:1bytes
- 数据输入:
cin >> a;
3、运算符
- 加减乘除:两个整数相除得到的依然是整数
- 取余:小数不能取余
- 自增自减:前置自增自减效率更高
- 赋值运算符
- 比较运算符:
cout << (a==b) << endl;
输出表达式时要带括号; - 逻辑运算符:C++ 中逻辑运算符返回
true/false
(这跟Python中and/or/not
有些区别)
C++:
3 && 2
返回1
;Python:3 and 2
返回 2!
4、程序流程结构
- 顺序结构
- 选择结构
- if 语句
- 三目运算符:
a>b?a:b;
它返回的是变量,不是变量的值,a>b?a:b=100;
是正确的; - switch 语句:
swicth...case...break...default
,尤其注意不能少了break
;
if 和 switch 的区别:
(1)if 可以判断区间,switch 只能判断整型或字符型,不能是区间;
(2)switch 结构清晰,执行效率比 if 高;
Switch 不允许一个 case 使用另一个 case 后声明定义的变量,错误信息为
jump bypasses variable initialization
!
- 循环结构
- while:
- do…while:
- for:初始化语句只执行一次,判断条件之后进入循环体,循环体执行完后再执行自加语句,再执行判断语句,…
- 跳转语句
- break:switch 中终止 case;循环语句中终止最近的循环;
- continue:在循环语句中,跳过本次循环,继续执行下一次循环;
- goto:跳转到标记的位置;
cout << "aaa" << endl;
goto FLAG;
cout << "bbb" << endl; // 被跳过
cout << "ccc" << endl; // 被跳过
FLAG;
cout << "ddd" << endl;
5、数组
-
数组的内存是连续的!
-
一维数组
- 定义:
// 三种定义方式 int a[10]; int b[10] = {0, 1, 2, 3}; int c[] = {0, 1, 2, 3};
-
数组名的作用:
-
查看数组的内存大小:
sizeof(a)
-
查看数组的地址:
a
返回的就是数组的首地址,也就是a[0]
的地址;a == &a[0]
; -
注意:数组名是常量(也就是数组的地址),不能修改;
a=100
是错误用法!
-
-
数组元素个数(长度):
int a_length = sizeof(a) / sizeof(a[0]);
;字符数组char a[] = "123"
可以用strlen
;
-
二维数组
- 定义:
// 四种定义方式 int a[2][3]; int b[2][3] = {{0, 1, 2}, {3, 4, 5}}; int c[2][3] = {0, 1, 2, 3, 4, 5}; int d[][3] = {0, 1, 2, 3, 4, 5};
- 数组名的作用:
- 查看二维数组所占用的空间大小:
sizeof(a)
- 获取二维数组的首地址:
a
返回的就是二维数组的首地址,也就是a[0][0]
的地址;a == a[0], a == &a[0][0]
;
- 查看二维数组所占用的空间大小:
- 数组的行数、列数:
int a_row = sizeof(a) / sizeof(a[0]); int a_col = sizeof(a[0]) / sizeof(a[0][0]);
6、函数
- 参数传递类型:
- 值传递:
- 引用传递:传递的是对象的引用,形参和实参是同一对象
- 地址传递:传递的是对象的地址,形参和实参指向同一对象
- 函数的声明:
main()
中用到的函数必须在它之前有声明或者定义(跟Python不一样),否则报错!可以多次声明。 - 头文件与源文件:
- 头文件:
xxx.h
,引入iostream, std
等 - 源文件:
xxx.cpp
,引用头文件 - 主函数:
int main()
,引用头文件
- 头文件:
7、指针
- 指针的作用:通过指针间接访问内存(指针就是一个内存地址)
- 指针的定义、赋值、使用
int a = 10;
int *p = &a; // 定义,赋值
*p = 20; // 使用指针(解引用:在指针变量前面加一个 * 就是解引用)
- 内存大小:32 位系统占用 4 个字节;64 位系统占用 8 个字节(要获取指针的十进制数值,用
cout << (long)pt << endl;
) - 空指针:指针变量指向内存中编号为 0 的空间,
int *p1 = NULL;
此时p==0
;内存编号 0~255 是系统内存,不允许访问 - 野指针:指针变量指向非法内存空间,例如
int *p = (int *)0x1100;
- 指针与 const:
- const 修饰指针:常量指针,指针的指向可以更改,指针指向的值不能改;
- const 修饰常量:指针常量,指针的指向不可以改,指针指向的值可以改;
- const 修饰指针和常量:指针的指向和指针指向的值都不可以改;
int a = 10;
int b = 20;
int c = 30
const int * p1 = &a; // 常量指针,指向的值不能改
int * const p2 = &b; // 指针常量,指针的指向不能改
const int * const p3 = &c; //
- 指针和数组:
- 数组指针:
int (*p)[5];
,数组的指针(()
的优先级比[]
高,所以p
会先跟*
结合,构成一个指针) - 指针数组:
int *p[5];
,由指针组成的数组([]
的优先级比*
要高,所以p
会先跟[]
结合,构成一个数组)
- 数组指针:
// 数组跟指针的关系
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = a; // 指针指向数组,也就是第一个元素
cout << *p << endl;
p++; // 指针指向第二个元素
cout << *p << endl;
// 数组指针,指针数组
int (*p1)[5]; // 数组指针,数组的指针
int *p2[5]; // 指针数组,数组中每一个元素都是指针
- 指针和函数:形参的地址传递!!!
- 当数组以指针的形式传入函数时
- 函数如何定义形参:
void func(int *arr)
,定义一个指针即可 - 如何传递数组实参:
func(arr)
,直接传入数组名(也就是数组地址)即可 - 函数内部如何使用数组:
arr[i]
,将指针当做数组名一样使用即可(但是无法使用sizeof(arr)
获取数组内存大小)
- 函数如何定义形参:
- 当数组以指针的形式传入函数时
8、结构体
- 概念:用户自定义的数据类型,允许用户存储不同类型的数据
- 定义:
struct Student
{
string name;
int grade;
double math;
};
- 创建变量:
// 方式一:
Student a;
a.name = "张三";
a.grade = 2;
a.math = 92.5;
// 方式二:
Student b = {"李四", 3, 85.5};
// 方式三:在结构体定义的最后创建变量
- 结构体数组:数组中每个一元素都是结构体类型
- 结构体指针:通过指针访问结构体对象中的成员,但是要注意访问方式不是
.
而是->
.
和->
的联系和区别:(1)联系:
.
和->
都可以用来访问成员(2)区别:
.
是实体对象访问成员用的,左边必须是实体对象;->
是指针访问成员用的,左边必须是指针;
- 结构体与『函数–指针–const』:
- 结构体作为参数传入函数时,使用指针,可以节省内存空间(不必复制一份数据)
- 结构体指针加一个 const 传入函数时,可以防止在函数内部误操作,修改了结构体的值
二、C++ 核心编程
1、内存模型
- 内存四区:
- 代码区:存放函数体的二进制代码,由操作系统进行管理;共享,只读
- 全局区:存放全局变量、静态变量、全局常量(全局区包含常量区),在程序结束后由系统释放
- 栈区:存放局部变量、局部常量、函数参数值、函数返回值,由编译器自动分配、释放
- 堆区:存放new、malloc申请的内存,由程序员分配和释放(delete和free)
// new int(10) 创建整型数据,放在堆区,返回数据的指针
// int *p 是一个整型指针,放在栈区,指向堆区变量
int *p = new int(10); // new 创建变量
int *arr = new int[10]; // new 创建数组
不要返回局部变量的地址!!!编译器在局部变量使用完以后,会保留一次局部变量的使用权,所以在外部第一次使用局部变量的地址时,不会出错;但是这一次机会用完以后,放在栈区的局部变量就被销毁掉了,第二次再使用就会出错!
- 程序运行前有代码区、全局区的概念,程序运行后才会区分栈区、堆区
new/delete
的使用:new
返回的是所创建的数据的指针,可能是整数的指针,也可能是数组的指针delete
释放数组需要加一个[]
delete p;
delete[] arr; // 堆区数组的释放要加一个 []
2、引用
- 引用的本质:给变量起别名,在 C++ 内部它的实现是一个指针常量!
int a = 10;
int &ref_a = a; // a 的引用
int *const pt_a = &a; // a 的指针常量,其实就是引用,它的值跟 &a、&ref_a 是完全一致的
- 引用的注意事项:(1)引用必须初始化;(2)引用初始化以后不能更改;
int a = 10;
int &b = a; // b 是 a 的别名
int c = 20;
b = c; // 这个并不是改变引用的对象,而是一个赋值操作,将 b 所代表内存中的数据改为 c 的值,之后 a/b/c 都等于 20
- 引用做函数形参:引用传递,被调函数的形参作为局部变量在栈中开辟内存空间,在内存中存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
地址传递:变量,独立;可变,可空;替身,无类型检查;
引用传递:别名,依赖;不变,非空;本体,有类型检查;
- 引用做函数返回:
- 函数调用可以作为左值
- 不要返回局部变量的引用!!!(这同上面说的,不要返回局部变量的地址,是一样的)
总结:引用传递和地址传递,函数形参如何定义?函数调用如何传参?
关键:形参定义方式跟引用、指针的定义一样,函数传参方式跟引用、地址的赋值一样!!!
实例:
- 引用传递:
- 形参定义:
int function(int &a)
- 函数传参:
function(aa)
,接收参数就类似于int &a = aa;
,这其实就是引用的定义与赋值;- 地址传递:
- 形参定义:
int function(int *a)
- 函数传参:
function(&aa)
,接收参数就类似于int *a = &aa;
,这其实就是地址的定义与赋值;
- 常量引用:一般用在函数形参,防止误操作!它的性质是『只读,不可修改』
- 引用的对象必须是一个合法的内存空间(栈区,堆区)!!!
int &ref_a = 10; // 这是错误的,不能直接引用一个数值,数值是在常量区,不在栈区,也不在堆区
const int &ref_a = 10; // 这是对的,它相当于创建了一个缓存变量 tmp,再引用它:int tmp=10; const int &ref_a=tmp;
3、函数高级
- 函数默认参数:函数声明和函数实现只能有一个有默认参数!如果两个都有默认参数,编译器会出现重定义错误
- 函数占位参数:
- 在调用时,占位参数也必须有对应的实参;
- 占位参数也可以有默认值;
int func(int a, int); // 这里的第二个参数就是占位参数
func(10, 20); // 调用时必须传实参
int func1(int a, int = 10); // 占位参数的默认值
- 函数重载:函数名相同,函数参数不同(类型不同,或个数不同,或顺序不同)
- 函数返回值不同不能作为函数重载的条件!
- 引用作为函数重载的条件:
void func(int &a)
与void func(const int &a)
构成函数重载 - 函数重载碰到默认参数:
void func(int a)
与void func(int a, int b=10)
不能构成函数重载
4、类和对象
C++ 面向对象三大特性:封装,继承,多态!!!
(1)封装
-
权限问题:
public
:类内可以访问,类外也可以访问private
:类内可以访问,类外不可以访问protect
:类内可以访问,类外不可以访问protected
跟private
的区别体现在继承中,子类可以访问父类的protected
成员,但不可以访问private
成员
-
class
与struct
的区别:class
默认权限是private
,struct
默认权限是public
-
运算符重载:
bool operator==(const Cube &c);
(2)对象特性
a. 构造函数
- 按照参数分类:有参构造,无参构造
- 按照类型分类:普通构造,拷贝构造
- 默认构造函数:
Person();
- 拷贝构造函数:
Person(const Person &p);
,它的调用时机有以下三种:- 使用一个已经创建好的对象初始化另一个新的对象:
Person p2(p1);
- 函数形参使用值传递
- 函数以值的方式返回局部对象
- 使用一个已经创建好的对象初始化另一个新的对象:
- 默认构造函数:
Person p1; // 调用默认构造函数
Person p2(); // 编译器会认为是一个函数的申明,而不是调用默认构造函数
// 1. 括号法
Person p2(10); // 调用有参构造函数
Person p2(p1); // 调用拷贝构造函数
// 2. 隐式法
Person p3 = 10; // 调用有参构造函数
Person p3 = p2; // 调用拷贝构造函数
// 3. 显式法
Person p3 = Person(10); // 调用有参构造函数
Person p3 = Person(p2); // 调用拷贝构造函数
// 注意事项:
Person(10); // 匿名对象,在此行执行完后会立即调用析构函数,销毁该内存
Person(p3); // 错误,编译器会将其视为 Person p3; ,而 p3 在上面定义过了,所以会报错
// 因此,不要用拷贝构造函数初始化一个匿名对象!!!
创建一个类,编译器至少会添加四个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数(对属性值进行拷贝)
- 赋值运算符
operator=
,对属性值进行拷贝但是,如果自定义了有参构造函数,编译器则不会提供默认构造函数,但是会提供拷贝构造函数,此时
Person p;
报错;如果自定义了拷贝构造函数,编译器不再提供任何构造函数,需要自定义有参构造函数、默认构造函数;
- 析构函数:主要的功能是,将在堆区开辟的内存空间手动释放掉!如果堆区内存不手动释放,会造成内存泄漏。
- 浅拷贝与深拷贝:
- 浅拷贝:一般的赋值、拷贝都是浅拷贝
- 深拷贝:new在堆区开辟空间做的是深拷贝
- 注意:如果类中有在堆区开辟的属性,一定要在析构函数中释放,进而要在拷贝构造函数中用new做深拷贝!
- 初始化类属性列表:只针对于类中的函数、属性而言
当A类对象作为B类属性时,创建一个B类对象,会先调用A类构造函数,再调用B类构造函数;析构函数则相反!
b. 静态成员
- 静态成员变量:
- 所有对象共享同一份数据(不属于某一个对象,所以其访问方式可以是通过对象,也可以直接用类去访问)
- 在编译阶段分配内存(在全局区)
- 类内声明,类外初始化(为什么必须要初始化???)
- 有访问权限的区别,
private
静态成员变量在类外无法直接访问,public
可以
class Person{
public:
static int m_A; // 类内声明;权限为 public
}
int Person::m_A = 10; // 类外初始化
int main(){
Person p;
cout << p.m_A << endl; // 通过对象访问
cout << Person::m_A << endl; // 通过类访问
return 0;
}
- 静态成员函数:
- 所有对象共享同一个函数(跟静态成员变量一样,可以通过对象访问,也可以通过类直接访问)
- 只能访问静态成员变量
class Person{
public:
static int test_static_value;
static void test_static_func(){
cout << test_static_value << endl; // 这里不能使用 this 指针,this 指针只能用于非静态成员函数内部
}
}
c. this
指针
- 其本质是一个指针常量!
- 成员变量和成员函数分开存储,只有非静态成员变量才属于对象上
- C++ 编译器给每一个空对象分配一个字节的内存空间,有一个独一无二的内存地址;也就是说,如果对象
p
是空对象,那么sizeof(p)
就等于 1; - 通过
sizeof
函数可以查看对象的大小,非空对象的大小只跟它内部的非静态成员变量大小有关;
- C++ 编译器给每一个空对象分配一个字节的内存空间,有一个独一无二的内存地址;也就是说,如果对象
class Person{
public:
int aa;
}
Person p;
cout << sizeof(p) << endl; // 输出为4,因为对象 p 内部只有一个整型的非静态成员变量
-
非静态成员函数只有一份函数实例,存在代码区,所有的对象共用这一个函数实例,那如何区分是哪个对象调用了的这个非静态成员函数呢?就是用
this
指针去区分的。 -
this
指针指向被调用非静态成员函数所属的对象,也就是说:如果有Person p;
这样一个对象,当用p
调用非静态成员函数func()
时,func()
内部会有一个默认的this
指针,它指向对象p
(不会指向其他的Person
对象)。 -
this
指针的作用:- 解决非静态成员变量跟函数形参之间的名称冲突
- 返回对象本身(使用
*this
,返回类型可以是值,也可以是引用,二者是有区别的)
-
空指针调用成员函数:成员函数如果使用了成员变量,此时
this
指针会为空,导致调用失败;一般的解决方式是,在成员函数中加一个指针为空的判断,使得成员函数更加健壮。
class Person{
public:
int age;
void func(){
if(this == NULL){ // 判断this指针是否为空
return; // 如果不加这个判断,后面空指针调用该成员函数时,下面的代码会崩掉
}
cout << this->age << endl; // 用到了成员函数
}
}
Person *p = NULL;
p.func();
- 常函数:
const
修饰的成员函数,其内部的成员属性不能修改,除非使用关键字mutable
修饰- 常函数的
const
修饰的是this
指针的指向,本来this
指针就是指针常量,也就不能修改它的指向,现在再用一个const
修饰它的指向,也就是说它指向的值也不能修改,即成员变量不能修改!
- 常函数的
- 常对象:对象声明前加
const
修饰,它只能调用常函数,只能修改mutable
修饰的成员变量;
class Person{
public:
int m_A; // 常函数中不可修改的成员变量
mutable int m_B; // 常函数中可以修改的成员变量
void func() const{ // 常函数
this->m_B = 10; // 常函数中只能修改有mutable的成员变量
}
}
const PersonX p; // 常对象
p.m_B = 10; // 常对象可以修改有mutable的成员变量
p.func(); // 常对象只能调用常函数
(3)友元
让函数和类,能够访问另一个类的私有成员;使用 friend
关键字
- 全局友元函数:将全局函数声明写在类里面,前面加一个
friend
修饰 - 友元类:将类的声明写在另一个类里面,前面加一个
friend
修饰 - 成员友元函数:将类的成员函数写在另一个类里面,前面加一个
friend
修饰;
class Building
{
friend void building_friend(Building b); // 友元全局函数
friend class GoodGay01; // 友元类
friend void GoodGay02::visit(); // 友元成员函数
private:
string bed_room;
public:
string setting_room;
Building();
~Building();
};
- 要注意的一个点是:如果A类是B类的友元,A在定义B类对象时,B类必须在它之前有定义,或者是有声明;如果只有一个声明,那么在A类中定义B类对象时,就只能用指针或者引用,因为编译到这里时还没有发现B类的定义,不知道该类的内部成员,没有办法具体地构造一个对象。(参考)
class Building;
class GoodGay
{
private:
Building *b; // 这个地方只能用指针或者引用,在构造函数中相应的要用new手动创建对象
public:
GoodGay();
~GoodGay();
void visit();
};
(4)运算符重载
a. 加号运算符+
- 可以通过成员函数重载(推荐),也可以通过全局函数重载
Person operator+(Person p){...} // 成员函数重载,跟类有关,本质是 p1.operator+(p2)
Person operator+(Person p1, Person p2){...} // 全局函数重载,跟类无关,本质是 operator+(p1, p2)
b. 左移运算符<<
- 最常用的用途是输出某种类型的数据,
cout << ... << endl;
; - 对于自定义类,通常不会利用成员函数重载
<<
,因为这样没办法实现cout
在左边,所以只能用全局函数来重载<<
; - 全局函数重载
<<
时,又不能访问类中的私有成员变量,所以要将全局函数变为友元;
# 在.h中写全局函数的声明
class Person{
friend ostream &operator<< (ostream &cout, Person &p); // 将重载函数变为类的友元,方便访问私有成员
Private:
int m_A;
int m_B;
}
# 在.cpp中写全局函数的实现
ostream &operator<< (ostream &cout, Person &p){ // 返回类型是ostream,这是输出流对象的类型
cout << "m_A: " << p.m_A << " m_B: " << p.m_B; // 自定义输出内容、形式,也就是重载的核心功能
return cout; // 将cout返回,方便链式输出
}
- 注意:
c. 递增运算符++
- 前置递增运算符:先自加,再返回,所以它可以返回自身;
Person &operator++();
- 后置递增运算符:先返回,再自加,它返回的是原始值,并不是自加后的自身;
Perosn operator++(int);
class Perosn{
private:
int m_A;
int m_B;
public:
Perosn &operator++(); // 前置递增运算符重载,返回的是引用,形参为空
Person operator++(int); // 后置递增运算符重载,返回的是值,形参需要一个int占位参数(声明为后置),且必须是int
}
// 前置递增运算符重载的实现
PersonX &PersonX::operator++()
{
++(this->m_A); // 属性递增
++(this->m_B); // 属性递增
return *this; // 返回自身的引用
}
// 后置递增运算符重载的实现
PersonX PersonX::operator++(int)
{
PersonX tmp = *this; // 先保存原始值,后面需要返回
(this->m_A)++; // 属性递增
(this->m_B)++; // 属性递增
return tmp; // 返回原始值
}
- 注意:
- 一:因为后置递增运算符重载函数的返回是一个临时对象
tmp
,它只能是值传递,不能是引用传递; - 二:因为后置递增运算符重载函数的返回只能是值传递,那么左移运算符在调用后置递增运算符的结果时,传入的参数类型就不能是引用,所以它只能定义为
friend ostream &operator<<(ostream &cout, Person p);
,不能用Perosn &p
;
- 一:因为后置递增运算符重载函数的返回是一个临时对象
d. 赋值运算符=
-
如果有成员是在堆上创建的,就必须让赋值操作符重载,因为默认的赋值操作符是浅拷贝,会发生内存泄漏;
-
赋值操作符重载形式:
Perosn &operator=(Perosn &p);
;
class Person{
private:
int *m_A;
public:
Person(int a){
m_A = new int(a);
}
Person &operator=(Perosn &p); // 赋值运算符重载定义
}
Person &operator=(Person &p){
if (this->m_A != NULL){
delete this->m_A; // 一定要注意,如果堆区内存还存在,要先释放,防止内存泄漏
}
this->m_A = new (*p.m_A); // 将赋值操作转移到堆区
return *this; // 返回自身,方便进行链式赋值(a=b=c)
}
e. 关系运算符
- 基本形式:
bool operator==(Person &p);
f. 函数调用运算符()
- 重载之后的函数称为仿函数
// 案例一:打印类的函数调用运算符重载
class MyPrint{
public:
void operator()(string s){
cout << s << endl;
}
}
MyPrint mp;
mp("hello world!"); // 因为它的使用方式很像函数调用,但实际上它是一个运算符,所以叫做仿函数
// 案例二:加法类的函数调用运算符重载
class MyAdd{
public:
int operator()(int a, int b){
return a + b;
}
}
int a = b = 10;
MyAdd md;
int c = md(a, b); // 两个形参,返回整型值
(5)继承
-
基本语法:
class FootballPlayer : public PersonX
,class 子类 : 继承方式 父类
-
继承方式:
- 公共继承:对于父类中的成员,
public
还是public
,protect
还是protect
,private
不可访问 - 保护继承:对于父类中的成员,
public
变为protect
,protect
还是protect
,private
不可访问 - 私有继承:对于父类中的成员,
public
还是private
,protect
变为private
,private
不可访问
- 公共继承:对于父类中的成员,
-
继承中的对象模型:
- 父类中所有的非静态成员都会被子类继承下来,私有成员虽然不能访问,但是只是被编译器隐藏了,实际上是继承了的
- 具体对象占用多大内存,可以通过
sizeof()
查看
VS 中可以使用 cl 工具看对象内存模型(参考,
cl /d1 reportSingleClassLayout类名 文件名
),Mac+VSCode 要怎么看??? -
继承中的构造和析构顺序:先调用父类构造函数,再调用子类构造函数;析构顺序相反。
-
继承中同名成员处理:
- 子类对象访问同名的子类成员:直接
.
出来即可,son.age
- 子类对象访问同名的父类成员:需要指定作用域才能访问,
son.Base::age
- 对于同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的同名成员函数,要访问的话需要加作用域(对于不同名的父类成员函数,可以直接访问)
- 静态成员函数也是一样的处理;
- 子类对象访问同名的子类成员:直接
-
多继承语法:
class 子类 : 继承方式 父类1, 继承方式 父类2, ...
-
菱形继承:
- B和C都继承自A,D则同时继承了B和C,此关系称为菱形继承
- 问题:对于A中的成员,B和C都会继承,D则会同时继承B和C中的成员,存在两份同样的数据,重复了
- 解决:虚继承可以解决菱形继承带来的数据重复问题(数据只有一份,通过
vbtable
和vbptr
来访问)vbtable
:虚基类表vbptr
:虚基类指针,指向vbtable
// 菱形继承
class Animal{
public:
int m_Age;
}
class Sheep : public Animal{}
class Tuo : public Animal{}
class SheepTuo : public Sheep, public Tuo{}
SheepTuo st;
st.Sheep::m_Age = 18; // 菱形继承时需要加作用域才能访问继承成员
st.Tuo::m_Age = 28; // 此时 st 中有两份 m_Age,分别来自于 Sheep、Tuo
// 虚继承解决菱形继承的问题
class Animal{
public:
int m_Age;
}
class Sheep : virtual public Animal{} // 虚继承
class Tuo : virtual public Animal{} // 虚继承
class SheepTuo : public Sheep, public Tuo{}
SheepTuo st;
st.Sheep::m_Age = 18; // 第一次修改 st 的 m_Age
st.Tuo::m_Age = 28; // 第二次修改 st 的 m_Age,修改的是同一份数据
(6)多态
-
多态的类型:
- 静态多态:主要有两种,函数重载、运算符重载
- 动态多态:主要是一种,继承类与虚函数
- 区别:
- 静态多态的函数地址早绑定,也就是在编译阶段就确定了函数地址
- 动态多态的函数地址晚绑定,也就是在运行阶段才确定函数地址
-
动态多态:
- 动态多态的实现:
- 条件一:子类要继承父类,必须有继承关系
- 条件二:子类要重写父类的虚函数,所谓重写就是返回值、函数名、参数都一样
- 动态多态的实现:
class Animal
{
public:
virtual void speak(); // 虚函数
};
class Cat : public Animal
{
public:
void speak(); // 重写虚函数,不需要再加virtual
};
class Dog : public Animal
{
public:
void speak(); // 重写虚函数,不需要再加virtual
};
- 动态多态的本质:
-
首先父类中有虚函数存在时,其对象就会有一个
vfptr
,指向vftable
中的虚函数入口地址vfptr
:虚函数指针,属于对象内存模型中的一部分vftable
:虚函数表,里面存储的是虚函数的入口地址(父类是父类的,子类是子类的)
-
其次,当子类继承父类时,它也会继承
vfptr
,但是这个vfptr
指向的是子类的vftable
,不再是父类的vftable
,它的实现也就是子类的实现(也就是,子类重写虚函数时,它的vftable
就会替换成子类虚函数实现的入口地址) -
从下面的对象模型可以看到:
- 父类对象模型确实多了一个
vfptr
- 如果不重写虚函数,子类就会直接继承父类的
vfptr
,它指向的还是父类的vftable
,函数实现也是父类的 - 如果子类重写了虚函数,那么它的
vftable
就会被替换成子类自己的虚函数表,其函数实现就是子类自己的
- 父类对象模型确实多了一个
-
-
纯虚函数和抽象类:
- 纯虚函数:虚函数声明后面加
=0
,不需要实现 - 抽象类:包含纯虚函数的类,有两个特点:
- 抽象类不可以实例化对象
- 抽象类的子类必须要重写父类中的纯虚函数,否则子类也属于抽象类
- 纯虚函数:虚函数声明后面加
-
虚析构和纯虚析构:
-
虚析构:析构函数定义为虚函数
-
使用场景:子类中有成员创建在堆区,且父类指针指向子类对象,当手动
delete
父类指针时,父类指针无法调用子类析构函数,子类对象在堆区的成员就无法被释放掉,会造成内存泄露 -
使用方式:将父类析构函数定义为虚函数
-
注意事项:抽象类(有纯虚函数的父类)的析构函数不能是虚函数(只能是纯虚函数???)
class Animal { public: Animal() { cout << "Animal构造函数" << endl; } virtual ~Animal() // 虚析构 { cout << "Animal析构函数" << endl; } virtual void speak(){}; // 虚函数 }; Animal *animal = new Cat("Tom"); // 父类指针指向子类对象(在堆区) animal->speak(); // 调用子类重写的虚函数 delete animal; // 手动释放内存(不然无法调用析构函数)
-
-
纯虚析构:析构函数定义为纯虚函数
- 纯虚析构的作用跟虚析构是一样的,都是为了让父类指针调用子类对象的析构函数,从而释放子类对象的堆区成员
- 纯虚析构需要写函数实现,这一点跟纯虚函数是不一样的
- 拥有纯虚析构的类也属于抽象类,无法实例化对象
class Animal { public: Animal() { cout << "Animal构造函数" << endl; } virtual ~Animal() = 0; // 纯虚析构 virtual void speak() = 0; // 纯虚函数 }; // 纯虚析构必须要写函数实现 Animal::~Animal() { cout << "Animal析构函数" << endl; }
-
5、文件操作
(1)文本文件读写
C++ 操作文件需要包含 fstream
头文件,主要有三个大类:
ofstream
:将数据写入文件操作,out
输出到文件
#include <fstream>
ofstream ofs;
ofs.open("文件路径", 打开方式);
ofs << "写入内容" << endl;
ofs.close();
ifstream
:从文件中读取数据
#include <fstream>
ifstream ifs;
ifs.open("文件路径", 打开方式);
if(ifs.is_open()){
// 四种读取方式
ifs.close();
}
// 第一种读取方式
char buf[1024] = {}; // 读取到字符数组
while (ifs >> buf) // 逐行读入到buf中,读完返回false
{
cout << buf << endl;
}
// 第二种读取方式
char buf[1024] = {};
while (ifs.getline(buf, sizeof(buf)))
{
cout << buf << endl;
}
// 第三种读取方式
string buf; // 读取到字符串
while (getline(ifs, buf)) // 逐行读取
{
cout << buf << endl;
}
// 第四种读取方式
char c; // 读取到字符中
while ((c = ifs.get()) != EOF) // 逐个字符读取
{
cout << c;
}
fstream
:读写文件数据
文件打开方式:打开方式可以配合使用,使用 |
连接就可以
打开方式 | 解释 |
---|---|
ios::in | 以只读方式打开文件 |
ios::out | 以写入方式打开文件 |
ios::ate | 打开文件,且初始位置在文件尾 |
ios::app | 以追加方式写入文件 |
ios::trunc | 如果文件存在,则先删除再创建 |
ios::binary | 二进制方式 |
(2)二进制文件读写
打开方式指定为ios::binary
,写入方式使用成员函数ostream& write(const char *buffer, int len);
。
ofstream ofs;
ofs.open("test04.txt", ios::binary | ios::out);
PersonX p("程", 28, 173);
ofs.write((const char *)&p, sizeof(p));
ofs.close();
读二进制文件使用成员函数istream& read(char *buffer, int len);
。