C++学习笔记

《黑马程序员》学习记录

一、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:类内可以访问,类外不可以访问
    • protectedprivate的区别体现在继承中,子类可以访问父类的protected成员,但不可以访问private成员
  • classstruct的区别class默认权限是privatestruct默认权限是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 函数可以查看对象的大小,非空对象的大小只跟它内部的非静态成员变量大小有关;
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返回,方便链式输出
}
  • 注意:
    • 一:左移运算符的重载模板比较固定,函数类型是全局友元函数,函数参数是两个引用,函数返回值是引用ostream使用引用,是为了全局只使用一个cout输出流对象;自定义类对象使用引用,是为了输出对象自身(如果使用值传递,那么它会调用拷贝构造函数,复制一份新的数据)。
    • 二:重载函数的声明与定义写在.h头文件中,实现最好写在.cpp源文件中,这跟C++中头文件和源文件的使用方式有关,如果实现也写在头文件中,在两个源文件main.cpptest.cpp都包含头文件的话,编译会报错,相当于重载函数被重定义了;分开写的话则不会有这个问题。(参考1参考2
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 PersonXclass 子类 : 继承方式 父类

  • 继承方式:

    • 公共继承:对于父类中的成员,public还是publicprotect还是protectprivate不可访问
    • 保护继承:对于父类中的成员,public变为protectprotect还是protectprivate不可访问
    • 私有继承:对于父类中的成员,public还是privateprotect变为privateprivate不可访问
    image-20221013201926772
  • 继承中的对象模型:

    • 父类中所有的非静态成员都会被子类继承下来,私有成员虽然不能访问,但是只是被编译器隐藏了,实际上是继承了的
    • 具体对象占用多大内存,可以通过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中的成员,存在两份同样的数据,重复了
    • 解决:虚继承可以解决菱形继承带来的数据重复问题(数据只有一份,通过vbtablevbptr来访问)
      • vbtable :虚基类表
      • vbptr:虚基类指针,指向vbtable
    image-20221016181457189
// 菱形继承
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就会被替换成子类自己的虚函数表,其函数实现就是子类自己的
iShot_2022-10-18_20.31.55 iShot_2022-10-18_20.31.55 iShot_2022-10-18_20.33.07
  • 纯虚函数和抽象类:

    • 纯虚函数:虚函数声明后面加=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);

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值