C++重点内容
文章目录
核心编程
针对C++的面向对象编程技术解释,了解C++核心和精髓
★★★ 1. 内存分区模型
C++程序在执行时,内存大致分为4个区域
- 代码区:存放函数体的二进制代码,由操作系统管理
- 全局区:存放全局变量和静态变量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配(new/malloc)和释放(delete/free),若程序员不释放,程序结束时由操作系统回收
1.1 程序运行前
程序编译后,生成.exe的可执行程序,未执行该程序前分为两个区
代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,目的是对于频繁执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,目的是防止程序意外地修改了它的指令
全局区:
- 存放全局变量和静态变量
- 包含常量区,字符串常量和其他常量存放于此
- 该区域的数据在程序结束后由操作系统释放
1.2 程序运行后
栈区:
- 由编译器自动分配释放,存放函数的函数的参数值、局部变量等
- 注意不要返回局部变量的地址,栈区开辟的数据由编译器自动释放,再访问该地址属于非法操作。(vs的编译器在第一次访问时会保留局部变量的值,第二次才会出错,但是Linux下的g++编译器不会保留)
堆区:
- 由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟内存
1.3 new关键字
利用new在堆区开辟内存,利用delete手动释放内存
利用new创建的数据,会返回该数据对应类型的指针,即返回数据所在的地址,并用指针接收
delete指针后,系统只会回收内存空间,而不会删除指针,此时指针将变成野指针,指向这块已经被系统回收的非法内存空间。解决方法是在delete之后将指针置为空指针,保证内存安全
new创建数组:int * arr = new int[10]
使用数组时可以利用索引的方式访问每个数组的每个元素,因为指针就是数组,数组就是指针
delete释放数组:delete[] arr
必须加[]
才能释放整个数组,不然只会释放数组第一个元素,剩下的元素都会丢失导致内存泄露
2. 引用
2.1 基本使用
作用:给变量起别名数据类型 &别名 = 原名
引用必须初始化,且初始化后不可改变
2.2 引用作函数参数
函数传参时,可以利用引用让形参修饰实参,即改变形参的值,实参也会同样变化
引用传递:void func(int &a)
原理是函数中使用别名接受实参,使得形参实际上是实参的一个别名,所以改变形参会导致实参发生改变,达到形参修饰实参的效果
2.3 引用作函数的返回值
不要返回局部变量的引用,局部变量在函数调用结束后释放内存,此时使用返回的引用访问内存是非法操作,原因同1.2 程序运行后 栈区第二条
函数调用可以作为左值
int & func()
{
static int a = 10;//由于不能返回局部变量引用,所以使用静态变量
return a;
}
int main()
{
int &b = func();
func() = 20;//a=20
cout<<b<<endl;//b=20
}
func()=20
因为函数返回的是变量的引用,所以对函数调用赋值相当于对变量a赋值
★ 2.4 引用的本质
**本质:**引用在C++的内部实现是一个指针常量
★ 2.5 常量引用
作用:用来修饰形参,防止形参改变实参
常量引用:int &a = 10
是错误的语法,因为引用必须引用一块合理的内存空间;const int &a = 10
是正确的语法,因为编译器会自动创建一个值为10的临时变量int tmp = 10
,并将a设置为该变量的别名const int &a = tmp
。这时用户只能通过别名去操作内存,因为原名是编译器创建的,用户并不知道原名是什么。但是加了const以后在合法的同时引用也变成只读属性,禁止对所引用对象的一切修改
当常量引用作为函数参数,形参就不能发生改变,从而保护了实参不会发生改变
常量引用可以指向非常量对象,但不允许通过该引用修改非常量对象的值。不过允许直接给被引用的对象赋值或者再绑定一个普通引用来修改其值
3. 函数提高
3.1 函数的默认参数
- 形参可以有默认值
- 若传入了参数数据,则用传入的数据;若没有,则使用默认值
- 如果某个位置已经有默认参数,那么之后的参数都必须有默认值
- 函数声明和实现最多只能有一个有默认参数
3.2 函数占位参数
函数形参列表中可以有默认参数,调用函数时必须填补该位置
占位参数:返回值类型 函数名(数据类型){}
占位参数还可以有默认参数,此时调用时可以不用填补占位参数的位置
3.3 函数重载
作用:函数名可以相同,提高复用性
函数重载条件:
- 同一个作用域下
- 函数名相同
- 函数参数类型不同或个数不同或顺序不同
函数的返回值不能作为函数重载的条件
注意:C++中未注明类型的浮点数被认为是double类型
引用作形参的函数重载:当引用作函数参数时,普通的引用形参和常量引用形参之间可以重载
函数重载与函数默认参数:当存在默认参数时,可能导致函数无法重载
void func(int a, int b = 10)
{
cout<<"func(a,b)"<<endl;
}
void func(int a)
{
cout<<"func(a)"<endl;
}
int main()
{
func(10);
}
此时由于func(10)
既可以调用func(int a, int b=10)
的函数,也可以调用func(int a)
的函数,出现了函数二义性,导致程序出错
4. 类和对象
C++面向对象的三大特点:封装、继承、多态
C++中认为万物皆为对象,对象有其属性和行为
4.1 封装
4.1.1 封装的意义
意义:①将属性和行为作为一个整体,表现生活中的事物;②将属性和行为加以权限控制
意义①:
在设计类时,属性和行为写在一起,表现事物
语法:class 类名{ 访问权限: 属性 / 行为};
实例化:通过一个类来创建一个具体的对象
get和set函数:用于通过类中公有的成员函数访问私有的成员变量
意义②:
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限:
- public公共权限:类内可以访问,类外也可以访问
- protected保护权限:类内可以访问,类外不可以访问,子类可以访问父类的保护内容
- private私有权限:类内可以访问,类外不可以访问,子类不可以访问父类的私有内容
4.1.2 struct和class区别
在C++中struct和class区别在于默认访问权限不同。struct的默认权限是public,class的默认权限是private
struct中可以包含成员函数,但并不常这么做
一般的结构体变量和类对象访问成员变量时使用obj.var
的格式;当使用结构体指针和类指针访问成员变量时使用obj->var
的格式
注意:类对象的私有成员可以通过指针修改。在类中定义一个公共权限的成员函数,返回值为私有成员的地址,在类外用一个指针指向类对象成员函数的返回地址,然后通过指针访问对象的私有成员变量,是可以对该变量进行读写操作的。所以尽量不要使用指针访问私有成员,容易引起内存问题和数据异常
4.1.3 成员属性设置为私有
优点:将所有成员设置为私有,可以自己控制读写权限;对于写权限可以检测数据的有效性
可以用get和set函数对私有成员进行获取和赋值
4.1.4 多文件编写类
将类的声明和定义分开放到多个文件中,在使用类时进行导入
新建一个头文件point.h,包含Point类的声明
#prama once//防止头文件重复包含
#include<iostream>
using namespace std;
class Point
{
public:
void setX(int x);
void setY(int y);
int getX();
int getY();
private:
int x;
int y;
};
新建一个源文件point.cpp,包含Point类成员函数的定义
#include"point.h"
void Point::setX(int x)
{
this->x = x;
}
void Point::setY(int y)
{
this->y = y;
}
int Point::getX()
{
return x;
}
int Point::getY()
{
return y;
}
这样类的多文件编写就完成了,使用Point类时只需要导入头文件#include"point.h"
即可,注意导入类的时候用的是分号而不是尖括号
4.2 对象的初始化和清理
4.2.1 构造函数和析构函数
C++中如果你不提供构造函数和析构函数,编译器会提供一个空实现的构造函数和析构函数
构造函数:在创建对象时为对象的成员赋值,由编译器自动调用
语法:类名(){}
没有返回值也不写void;函数名和类名一样;可以有参数,可以重载;无需手动调用,只调用一次
析构函数:在对象销毁前自动调用,执行清理工作
语法:~类名(){}
没有返回值也不写void;函数名和类名一样,名称前加~;没有参数,不可重载;无需手动调用,只调用一次
4.2.2 构造函数的分类和调用
分类:按参数分为有参构造(默认构造)和无参构造;按类型分为普通构造和拷贝构造
调用:括号法、显示法、隐式转换法
拷贝构造函数:
利用一个已经实例化的对象来构造一个新的对象,两个对象的成员属性都一样
格式:Point(const Point &p){函数体}
参数最好使用常量引用修饰。因为如果使用值传递,调用拷贝构造函数时会在内存中复制一份形参,这时又会调用拷贝构造函数,造成无限递归直至栈满溢出。而使用引用传参则不会发生复制实参到形参的过程,自然不会产生无限递归;因为调用拷贝构造函数目的是为了使新构造的对象和原来的对象一模一样,所以不希望原对象在拷贝过程中发生改变,需要加上const修饰防止修改实参
拷贝构造函数的形参必须用引用传值,尽量用const修饰
括号法:Point p
或Point p(10,10)
或Point p(p0)
调用默认构造函数时,不要加括号,因为会被编译器当作函数声明,而不是创建对象
显示法:Point p
或Point p = Point(10,10)
或Point p = Point(p0)
其中等号右边的Point(arg)
称为匿名对象,特点为当前行执行结束后立即回收
不要利用拷贝构造函数初始化匿名对象Point(p0)
,编译器会当作对象声明Point p0
隐式转换法:Point p ={10,10}
或Point p = p0
当使用多参数创建对象时,使用大括号括起来
★ 4.2.3 拷贝构造函数调用时机
- 使用一个已经创建完的对象初始化一个新对象
Point p(10,10);
Point p1 = p
- 值传递的方式给函数参数传值:值传递时会将实参复制到形参当中,会调用拷贝构造函数
void test1(point p)
{
cout<<"test1";
}
void test2()
{
Point p1(10,10);
test1(p1);
}
- 以值的方式返回局部对象:C++中是通过临时复制对象来进行返回局部对象的,在将返回值复制到临时对象中时会调用拷贝构造函数。
Point test1()
{
Point p(10,10);
return p;
}
void test2()
{
Point p1 = test1();
}
注意:理论上来说,返回值复制临时对象调用一次拷贝构造函数,在赋值时又调用一次拷贝构造函数,一共会调用两次拷贝构造函数,并且p1和p的内存地址也会不同。但是实际执行时,可能出现不调用拷贝构造函数,并且p1和p的内存地址相同的情况。这是因为编译器开启了C++的RVO(返回值优化),为了避免不必要的临时对象节省空间提高运行效率。当关闭RVO时即可看到两次拷贝构造函数的调用过程
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加默认构造函数、默认析构函数、默认拷贝构造函数
如果用户定义有参构造函数,编译器不会提供默认构造函数,但是会提供默认拷贝构造函数
如果用户定义拷贝构造函数,编译器不会提供其他构造函数
★★★ 4.2.5 深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作。编译器提供的默认拷贝函数中的赋值操作都是浅拷贝
深拷贝:在堆区重新申请空间,进行拷贝操作
浅拷贝的问题:堆区的内存重复释放。当定义的类中包含指针类型的属性,调用默认拷贝构造函数将p1复制到p2上的情况下,浅拷贝会将p1属性中指针指向的地址赋值给p2属性中的指针,使得p1和p2的指针指向同一块堆区的内存,对象销毁前调用析构函数时,析构函数释放对象属性中指针指向的堆区的内存空间释放(析构函数的作用:销毁对象,释放堆区中的内存)。此时p2会先释放属性中指针指向的堆区的内存,p1后释放属性中指针指向的堆区的内存,造成重复释放,出现异常。
**用深拷贝解决问题:**自定义一个拷贝构造函数,拷贝指针时,不是简单地拷贝地址,而是在堆区重新创建一块内存并存放相同的值,然后让指针指向这块内存
浅拷贝地址相同,深拷贝地址不同
class Person
{
public:
int a;
int *b;
Person(int a, int b)
{
this->b = new int(b);
this->a = a;
}
//当没有自定义拷贝构造函数时,会使用浅拷贝。浅拷贝时this->b = b;
//需要自定义深拷贝
Person(Person p)
{
this->a = p.a;
this->b = new int(*p.b);//重新开辟一块内存
}
};
void test()
{
Person p1(10);
Person p2(p1);//调用拷贝构造函数
}//结束时先释放p2,再释放p1
int main()
{
test();
}
4.2.6 初始化列表
作用:用来初始化类的属性
语法:constructor():atrr1(val1),atrr2(val2),atrr3(val3)...{}
这里的val
都是常量,使得初始化的属性都是固定值;constructor(int var1, int var2, int var3):atrr1(var1),atrr2(var2),atrr3(var3)...{}
这里加入了形参使得传入的值可以是变量
好处:①类成员中存在引用或常量,只能初始化不能赋值;
4.2.7 类对象为类成员
C++中类中的成员可以是另一个类的对象,称为对象成员
当其他类的对象作为本类的一个成员时,创建本类对象时会构造其他类的对象,后构造本类对象;析构的时候先析构本类对象,再析构其他类的对象
★ 4.2.8 静态成员
定义:在成员变量或成员函数前加上static
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化:初始化需要声明是哪个类作用域下的静态成员变量
- 静态成员变量也有访问权限
class Person
{
public:
static int a;//类内声明
private:
static int b;//静态成员变量也有访问权限,在类外无法访问
}
//类外初始化,需要加上Person作用域,避免被认为是全局变量
int Person::a = 100;
int Person::b = 200;
void test()
{
Person p;
cout<<p.a<<endl;//100,由于在类外进行了初始化,所以这里访问时为初始化后的值
Person p2;
p2.a = 200;
cout<<p.a<<endl;//200,p2修改了a的值,由于a在内存中只有一份,所以p和p2的成员变量a是同一个a,p2修改后,p访问到的a就是修改后的a
cout<<p.b<<endl;//语句报错,在类外无法访问到b
};
void test2()
{
//由于静态成员变量不属于某个对象,所以有两种访问方式
//1.通过对象访问
Person p;
cout<<p.a<<endl;
//2.通过类名访问
cout<<Person::a<<endl;
}
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量:由于静态成员函数只有一份,不属于某一个对象,所以如果静态成员函数中包含非静态成员变量,当调用此函数时,无法得知访问的是哪个对象的成员变量
- 静态成员函数也有访问权限
class Person
{
public:
static int a;//静态成员变量
int b;//非静态成员变量
static void func()
{
a = 200;//正确,静态成员函数可以访问静态成员变量
b = 200;//错误,静态成员函数中不能访问非静态成员变量
cout<<"static"<<endl;
}
};
int Person::a = 100;
void test()
{
//由于静态成员函数不属于某个对象,所以有两种访问方式
//1.通过对象访问
Person p;
p.func();
//2.通过类名访问
Person::func();
}
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
只有非静态的成员变量属于类的对象上:静态成员变量、非静态成员函数、静态成员函数都不在类的对象上
空对象(类定义为空的对象)占用空间为1个字节,因为C++编译器会给每个空对象也分配一个字节空间,为了区分空对象占内存的位置
4.3.2 this指针
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
this指针指向被调用的成员函数所属的对象,本质为指针常量,不可以修改其指向
this指针是隐含每一个调用非静态成员函数内的一种指针
this不需要定义,可直接使用
作用:
- 当形参和成员变量同名可以用this区分,解决名称冲突
- 在类的非静态成员函数中返回对象本身,可使用
return *this
。返回值需使用引用,否则返回的是原对象的拷贝对象
class Person
{
public:
int a;
void func(int a)
{
this->a = a;//利用a避免名称冲突
}
Person& add(Person p)//返回值必须是引用,如果不是引用,则会调用拷贝调用函数创建新的Person对象,后续操作改变的就不是p2,而是复制的新对象
{
this->a += p.a;
return *this;
}
};
void test()
{
Person p1(10);
Perosn p2(20);
p2.add(p1).add(p1).add(p1);//链式编程
}
链式编程:可以在代码后无限追加。例如输入输出流cout<<"link"<<"coding"<<endl;
4.3.3 空指针访问成员函数
C++空指针可以调用成员函数,但是要注意是否用到this指针,如果用到了需要加强代码的健壮性
class Person
{
public:
int a;
void func()
{
if(this == NULL)//当利用空指针调用该函数时,this为空,使用该语句避免报错,增加鲁棒性
{
return;
}
cout<<a;
}
};
void test()
{
Person * p = NULL;
p->func();//空指针也可以调用成员函数
}
★ 4.3.4 const修饰成员函数
成员函数后加const后称为常函数,其内不可以修改成员属性,成员属性加了关键字mutable后可以在常函数中修改
声明对象前加const称为常对象,常对象只能调用常函数
class Person
{
public:
//常函数
//加了const相当于const Person * const this,使this指针的指向和值都不可修改
void func() const
{
int b = 100;//加了mutable后可以在常函数中修改
cout<<"func"<<endl;
}
mutable int b;
};
void test()
{
const Person p;//常对象
p.b = 200;//加了mutable后可以通过常对象修改
p.func();//常对象只能调用常函数。因为普通成员函数可以修改属性
}
4.4 友元
适用:类中有些私有属性想要类外一些特殊的函数或类进行访问
关键字:friend
实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
class Building
{
friend void goodGay(Building *bd);//声明该函数为友元函数
public:
string a;
private:
string b;
};
void goodGay(Building *bd)
{
cout<<bd->b<<endl;//友元函数可以访问该类的私有成员
}
4.4.2 类做友元
class Building;
class GoodGay
{
public:
void visit();//访问Building中的属性
Building * bd;
}
class Building
{
friend class GoodGay;//声明该类为友元类
private:
stirng a;
}
GoodGay::visit()
{
cout<<bd->a<<endl;
}
4.4.3 成员函数做友元
class Building;
class GoodGay
{
public:
void visit();//访问Building中的私有成员
Building * bd;
};
class Building
{
friend void GoodGay::visit();//声明该成员函数为GoodGay作用域下的友元成员函数
private:
string a;
};
GoodGay::visit()//因为需要使用Building类型的指针,所以需要在Building类之后定义
{
cout<<bd->a<<endl;
}
4.5 运算符重载
4.5.1 加号运算符重载
实现两个自定义类型相加的运算
运算符重载也可以发生函数重载
对于内置的数据类型的表达式的运算符是不可能改变的
class Person
{
int a;
int b;
//成员函数重载运算符
Person operator+(Person &p)
{
Person tmp;
tmp.a = this->a + p.a;
tmp.b = this->b + p.b;
return tmp;
}
};
//全局函数重载运算符
Person operator+(Person &p1, Person &p2)
{
Person tmp;
tmp.a = p1.a + p2.a;
tmp.b = p1.b + p2.b;
return tmp;
}
int main()
{
Person p1,p2;
Person p3 = p1 + p2;//成员函数调用,p1+p2本质为p1.operaator+(p2)
Person p3 = p1 + p2;//全局函数调用,p1+p2本质为operator+(p1,p2)
}
4.5.2 左移运算符重载
不会使用成员函数重载左移运算符,因为无法实现cout在左侧。所以使用全局函数重载
class Person
{
friend ostream & operator<<(ostream &cout, Person &p)
private:
int a;
int b;
};
//全局函数重载运算符
//参数列表中cout在左,p在右(这里cout可以使用其他名称,因为引用相当于起别名),使用引用时因为ostream对象只能有一个
//返回类型是cout的引用,因为这样调用时可以使用链式编程的思想进行调用
//如果需要访问类的私有成员,可以将全局函数设为该类的友元函数
ostream & operator<<(ostream &cout, Person &p)
{
cout<<"p_a:"<<p.a<<" p_b:"<<p.b;
return cout;
}
4.5.3 递增运算符重载
前置:++a表示先使a+1,再令表达式等于a
后置:a++表示先令表达式等于a,再使a+1
class MyInt
{
friend ostream& operator<<(ostream& cout, MyInt myint)
public:
MyInt()
{
n = 0;
}
//重载前置++运算符
//前置递增返回引用
MyInt &operator++()//返回引用为了始终对一个对象进行操作,实现链式编程
{
n++;
return *this;
}
//重载后置++运算符
//后置递增返回值,不能返回局部变量的引用
MyInt operator++(int)//int为占位参数,区分前置和后置
{
MyInt tmp = *this;
n++;
return tmp;
}
private:
int n;
};
//需要重载左移运算符,否则不能输出对象
ostream& operator<<(ostream& cout, MyInt myint)//参数中的MyInt类型不能使用引用,否则进行使用后置递增时会出现引用局部变量的情况
{
cout<<myint.n;//因为访问了类的私有成员变量,所以需要使用友元
}
void test()
{
MyInt myint;
cout<<++(++myint)<<endl;//正确,因为前置递增实现了链式编程
cout<<(myint++)++<endl;//错误,因为后置递增不能进行链式编程
}
4.5.4 赋值运算符重载
赋值运算符进行的是值拷贝。如果类中有属性指向堆区,进行赋值操作时会发生深浅拷贝问题,见4.2.5深拷贝与浅拷贝
class Person
{
public:
Person(int a)
{
this->a = new int(a);
}
~Person()
{
if(a != NULL)
{
delete a;
a = NULL;
}
}
//赋值运算符重载
Person& operator=(Person &p)//返回值为对象自身,实现链式编程
{
//判断是否有属性在堆区,有就释放,再进行深拷贝
if(a != NULL)
{
delete a;
a = NULL;
}
//深拷贝
a = new int(*p.a);
return *this;
}
int *a;
};
void test()
{
Person p1(18);
Person p2(20);
p2 = p1;//默认为浅拷贝,导致p1和p2的指针地址一样
//析构时导致p1和p2的指针指向的地址重复释放
}
4.5.6 关系运算符重载
注意:运算符左边的对象调用运算符,右边的对象作为运算符参数
重载格式:bool operator==(ClassName &obj)
以相等运算符为例
★ 4.5.7 函数调用运算符重载(仿函数)
函数调用运算符:()
重载后使用的方式非常像函数的调用,称为仿函数,没有固定格式和写法
匿名函数对象函数调用:ClassName()(args)
进行匿名函数对象的函数调用,且函数调用运算符是经过重载的。其中ClassName()
即为匿名对象,相当于创建了一个ClassName类的对象obj,原式就等价于obj(args)
class MyPrint
{
public:
//重载函数调用运算符()
void operator()(string a)
{
cout<<a<<endl;
}
};
void test()
{
MyPrint mp;
//仿函数
mp("hello world");//输出为hello world
}
4.6 继承
继承是面向对象的三大特性之一
4.6.1 继承的基本语法
语法:class ChildClass : 继承方式 FatherClass
子类也称为派生类,父类也称为派生类
4.6.2 继承方式
分类:公共继承、保护继承、私有继承
子类可以继承父类的私有属性,但无法访问
**protected修饰符:**保护成员在子类中可以访问,在类外不可以访问
4.6.3 继承中的对象模型
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
class Son : public Base
{
public:
int d;
};
int main()
{
sizeof(Son);//16,相当于对象模型中包含abcd四个成员
}
父类中所有非静态成员属性都会被子类继承下去,包括父类中的私有成员属性,只是在子类中被编译器隐藏了,子类无法访问
查看对象模型的属性布局:在vs开发人员命令提示符中通过cd定位到源文件所在文件夹cl /d1 reportSingleClassLayout类名 "文件名.cpp"
4.6.4 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数。并且是先构造父类对象,再构造子类对象;析构时先析构子类对象,再析构父类对象
4.6.5 继承同名成员处理方式
当子类与父类出现同名的成员时,子类访问同名成员的方法:
- 访问子类同名成员,直接访问即可,即用
子类.同名成员
方式访问 - 访问父类同名成员,需要加作用域,即用
子类.父类::同名成员
方式访问
当子类中出现同名的成员函数,子类的成员函数会隐藏掉父类中所有的同名函数(包括重载函数)。想要访问父类中重载的成员函数,同样需要加作用域
4.6.6 继承同名静态成员处理方式
和同名非静态成员处理方式一致,见4.6.5 继承同名成员处理方式
通过类名访问:
- 访问子类同名成员:
Son::a
- 访问父类同名成员:
Base::a
直接通过父类类名访问;Son::Base::a
通过子类类名访问父类作用域下的成员
4.6.7 多继承语法
C++允许一个类继承多个类
语法:class Son : 继承方式 Father1, 继承方式 Father2...
开发中不建议使用多继承
当父类之间出现同名成员,需要加作用域区分
★ 4.6.8 菱形继承
概念:两个派生类继承同一个基类,又有某一个类继承这两个派生类
当菱形继承两个父类的相同数据,但只需要其中一份的情况时利用虚继承解决:
class Animal
{
public:
int a;
}
class Sheep : virtual public Animal {};//继承前加virtual代表虚继承
class Camel : virtual public Animal {};//此时公共的父类Animal称为虚基类
class Alpaca : public Sheep, public Camel {};
void test()
{
Alpaca al;
al.a;//虚继承后通过该语法访问不会产生二义性
}
此时通过Alpaca类去访问a的数据只有一份,并且可以用al.a
的语法访问,因为不会产生二义性,也避免了资源浪费。
虚继承的底层原理:未使用虚继承时,Alpaca中的a是从Sheep类和Camel类继承而来的,一共有两份数据。使用虚继承后,Alpaca从Sheep类和Camel类分别继承了一个虚基类指针vbptr。同一个类的vbptr指向同一个虚基类表vbtable,vbtable中存放的是一个偏移量,该偏移量加上vbptr的地址找到的就是唯一的从Animal类虚继承下来的成员a
★★★ 4.7 多态
★ 4.7.1 多态的基本概念
多态是C++面向对象三大特性之一
分类:
- 静态多态:函数重载和运算符重载,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:静态多态的函数地址早绑定,即编译阶段确定函数地址;动态多态的函数地址晚绑定,即运行阶段确定函数地址
C++中允许父子之间的类型转换,不需要进行强制类型转换,父类的引用或指针可以直接指向子类对象
class Animal
{
public:
virtual void speak()//加了virtual关键字后,函数变成虚函数,实现函数晚绑定,实现动态多态
{
cout<<"Animal speaking"<<endl;
}
};
class Cat : public Animal
{
public:
virtual void speak()//子类中的virtual关键字可写可不写,无影响
{
cout<<"Cat speaking"<<endl;
}
};
void doSpeak(Animal & animal)
{
//如果没有加virtual,会导致函数地址早绑定,编译阶段确定函数地址,即编译时就已经绑定了Animal的speak()
animal.speak();
}
int main()
{
Cat cat;
dospeak(cat);//允许父类引用或指针直接指向子类对象
}
动态多态满足条件:
- 有继承关系
- 子类必须重写父类的虚函数
- 使用是让父类的指针或引用指向子类的对象
重写:函数返回值、函数名、参数列表完全相同,只有函数体不同称为重写。一般发生在子类重写父类的函数的情况下
多态的使用:Animal * animal = new Cat
利用父类指针指向子类的对象,用完记得delete销毁
动态多态底层原理:Animal类内部在成员函数speak()前面加上virtual关键字变成虚函数后,Animal类的内部结构多存放了一个虚函数指针vfptr,同一个类的vfptr指向同一个虚函数表vftable。vftable内部存放的是虚函数Animal::speak的入口地址,子类Cat继承时会继承父类的vfptr和vftable。此时Cat的vftable中存放的是虚函数Animal::speak的入口地址。当子类Cat类重写了父类的虚函数speak()时,子类会把自身内部结构vftable中的虚函数替换成重写后的函数。当使用父类指针指向子类对象并调用同名函数时,会从子类的虚函数表中找到重写后的函数入口地址
4.7.2 纯虚函数和抽象类
多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类中重写的函数。因此可以将虚函数改为纯虚函数
纯虚函数:virtual 返回值类型 函数名 (参数列表) = 0;
含有纯属函数的类称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类的纯虚函数,否则也属于抽象类,无法实例化对象
4.7.3 虚析构和纯虚析构
多态使用时,如果子类有属性开辟在堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类的析构函数改为虚析构或纯虚析构
语法:virtual ~ClassName(){}
虚析构函数;virtual ~ClassName() = 0;``ClassName::~ClassName(){}
纯虚析构函数,必须在类外实现
共性:可以解决父指针释放子类对象的问题;都需要有具体的函数实现
区别:包含纯虚析构的类是抽象类,无法实例化对象
如果子类中没有堆区数据,可以不写虚析构或纯虚析构
5. 文件操作
5.1 文本文件
通过文件对数据进行持久化
需要包括头文件#include<fstream>
分类:
- **文本文件:**以ASCII码形式存储
- **二进制文件:**以二进制形式存储
操作:
- ofstream:写操作的类
- ifstream:读操作的类
- fstream:读写操作的类
5.1.1 写文件
步骤:
- 包含头文件
#include<fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open("文件路径",打开方式);
- 写数据
ofs<<"写入的数据";
- 关闭文件
ofs.close();
创建流对象和打开文件可以合并为ofstream ofs("文件路径",打开方式);
直接调用ofstream的有参构造函数
打开方式:
ios::in
:为读文件而打开文件ios::out
:为写文件而打开文件ios::ate
:初始位置在文件尾ios::app
:追加方式写文件ios::trunc
:如果文件存在先删除,再创建ios::binary
:二进制方式
使用多个打开方式使用 | 操作符。如,ios::binary |ios::in
5.1.2 读文件
步骤:
- 包含头文件
#include<fstream>
- 创建流对象
ifstream ifs;
- 打开文件
ifs.open("文件路径",打开方式);
。判断文件是否打开成功ifs.is_open()
- 读数据(四种方式)
- 关闭文件
ifs.close();
读数据的方式:
//1.char[]来接收ifs的数据流
//一个字符一个字符的读,读到空格和换行都会换行
char buf[1024];
while (ifs >> buf)
{
cout<<buf<<endl;
}
//2.利用函数ifs.getline(char[])将文件读入char[]
//getline函数会一行一行读,但是不会将换行读入,输出时需要自行添加换行
//此处的getline函数为ifs对象中的成员函数
char buf[1024];
while(ifs.getline(buf,sizeof(buf)))
{
cout<<buf<<endl;
}
//3.利用函数getline(ifs,string)将从数据流读入string
//getline函数特点如上,但是此处的函数是全局函数,需要包含头文件#include<string>
string s;
while (getline(ifs,s))
{
cout<<s<<endl;
}
//4.利用函数ifs.get()将文件读入char
//一个字符一个字符的读,会自动换行,读到EOF(End of File)就会退出循环
char c;
while((c = ifs.get()) != EOF)
{
cout<<c;
}
5.2 二进制文件
打开二进制文件时需要在打开方式中加ios::binary
5.2.1 写文件
步骤:
- 包含头文件
#include<fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open("文件路径",打开方式);
- 准备好要写的数据
Person p;
并进行初始化 - 写数据
ofs.write((const char *)&p,sizeof(p));
- 关闭文件
ofs.close();
5.2.2 读文件
步骤:
- 包含头文件
#include<fstream>
- 创建流对象
ifstream ifs;
- 打开文件
ifs.open("文件路径",打开方式);
。判断文件是否打开成功ifs.is_open();
- 准备接受读的对象
Person p
; - 读数据
ifs.read((char *)&p,sizeof(Person));
因为需要改变指针p中的值,所以不加const - 关闭文件
ifs.close();