1、内存分区模型(四个)
不同区域存放的数据,赋予不同的生命周期。
代码区:代码,操作系统管理
全局区:存放全局变量、静态变量、全局常量,程序运行结束后系统释放
栈区:编译器自动分配释放,存放函数的参数值,局部变量。
堆区:程序员分配和释放,若程序员不释放,程序结束时操作系统回收。
程序执行前分为代码区和全局区
程序运行后分为栈区和堆区
栈区注意事项:不要返回局部变量的地址,因为会由编译器自动释放。
堆区:C++主要利用new在堆区开辟内存。
new int(10);//new返回一个位于堆中的数据“10”所在内存的首地址
int* p = new int(10);//由于返回的是首地址,因此用指针变量接收
想释放堆区数据,利用delete关键字:
delete p;//释放堆内内存
在堆区new一个数组:
new int[10];//数组有十个元素,返回数组首地址
int * arr = new int[10];
释放数组:
delete [] arr;//释放数组
2、引用:给变量起别名
int a = 10;
int &b = a;//b是一个引用
注意:引用必须初始化,且一旦初始化就无法更改
int &b;//错误的
引用做函数参数:利用引用让形参修饰实参,简化指针修改实参。
以swap函数为例:
```cpp
//交换
//1、值传递,形参不会修饰实参
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//2、地址传递,形参要修饰实参
void swap2(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//3、引用传递,形参也会修饰实参
void swap3(int& a,int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
swap1(a, b);
cout << "a = " << a << "b = " << b << endl;//值传递,形参不会修饰实参
swap2(&a, &b);
cout << "a = " << a << "b = " << b << endl;//地址传递,形参要修饰实参
swap3(a, b);
cout << "a = " << a << "b = " << b << endl;//引用传递,形参也会修饰实参
return 0;
}
结果:
可见引用也可以让形参修饰实参,并且比指针传递地址简单。原因是swap3的参数&a,&b就是实参a,b的别名,引用变量是变量的另一个别名,它没有自己的存储数据的内存位置,它访问的是另一个变量的内存位置。对引用变量作出的任何更改,实际上都是对它所引用的变量内存位置中存储数据的更改。使用引用变量作为形参时,它将变为实参列表中相应变量的别名,对形参进行的任何更改都将真正更改正在调用它的函数中的变量。
引用做函数返回值:
注意:1)不要返回局部变量的引用,因为局部变量会被编译器释放;
int& test1() {
int a = 10;//局部变量
return a;
}
int main() {
int& ref = test1();//错误,不能返回局部变量的引用
return 0;
}
2)引用做返回值的函数的调用可以作为左值。如果函数作左值,必须返回引用。
int& test2() {
static int a = 10;//静态变量,存放在全局区,可以返回它的引用
return a;
}
int main() {
int& ref = test2();//可以,ref相当于test2()函数中a的别名
test2() = 1000;//函数作左值
return 0;
}
引用的本质:C++内部实现是一个常量指针。
引申:C++的指针常量和常量指针:
1)指针常量:const int *p;
//定义一个指针常量。
指针常量在C++ primer中的翻译为指向变量的指针,顾名思义就是这个指针指向的地址存放着的是一个常量。
2)常量指针:int * const p;
//定义一个常量指针。
常量指针是值为常量的指针,也就意味着它指向的内存地址是不能改变的,但是内存里面的内容是可以改的。
区分这俩主要看主语,一个是常量,一个是指针。
回到引用,
int a = 10;
int& ref = a;//系统会自动将这行转换为int* const ref = &a;指针常量的指向无法修改,也就说明为什么引用不能修改。
ref是a的地址的指针常量。
ref = 20;//系统发现ref是引用,自动转换为*ref = 20;
常量引用: 用来修饰形参,防止误操作。
const int & ref = 10;
加上const后,编译器将代码转换为
int temp = 10;
const int & ref = temp;
使用场景:
void showValue(const int& val) {//const防止引用的值被修改
//val = 100;//防止修改
cout << "val = " << val << endl;
}
3、函数提高
1)函数默认参数
int sum(int a, int b, int c) {//无默认参数
return a + b + c;
}
int sum2(int a , int b=20, int c =10) {//b,c参数
return a + b + c;
}
注意:传入实参时,有实参用实参的值,无实参用默认值;
形参默认值从左到右一旦开始有默认值,就必须给右边所有形参都赋默认值。不允许sum(int a = 10,b,c = 20);这样的方式;
如果函数声明有默认函数,则在函数实现时不允许还有默认值,防止两次给不同默认值。因此声明和实现只能其中一个有默认值。
2)函数占位参数
int example(int a, int , int ) {//占两个int位
cout << "占位例子" << endl;
}
int main() {
example(10, 10, 10);//调用时必须填补空缺的真实值
return 0;
}
3)函数重载:类似于Java的多个构造方法的原理,函数名相同,参数的类型、个数或顺序不同,提高函数复用性。
使用的满足条件:在同一作用域下;返回值类型不同作为重载的条件。
注意:
I、是否常量引用可以作为函数重载条件:
void func(int& a) {
cout << "func(int& a)调用" << endl;
}
void func(const int& a) {
cout << "func(const int& a)调用" << endl;
}
int main() {
const int a = 10;
int b = 10;
func(a);
func(b);
fun(10);
return 0;
}
运行结果:
II、函数重载碰到默认参数
void func(int a,int b = 10) {
cout << "调用" << endl;
}
void func(int a) {
cout << "调用" << endl;
}
系统认为第一个函数也许,第二个也行,因此产生了二义性,出错。
因此编程时应当避免重载函数出现默认参数。
4、类和对象
访问模式
拷贝构造函数调用时机
class Person
{
public:
Person() {};
Person(int age) {//(普通)构造函数
this->age = age;
}
Person(const Person& p) {//拷贝构造函数
this->age = p.age;
}
int age;
~Person() {//析构函数
cout << "析构函数调用" << endl;
}
};
1)、使用一个创建完毕的对象来初始化一个新对象。
void test1() {
Person p1(10);
Person p2(p1);
}
2)、值传递的方式给函数参数传值
void doWork(Person p) {
}
void test2() {
Person p;
doWork(p);//调用doWork时,将会调用一次拷贝构造函数拷贝副本
// 给doWork的形参传值
}
3、以值方式返回局部对象
Person doWork2() {
Person p;
return p;//返回时调用拷贝构造函数拷贝一个函数返回
}
void test3() {
Person p2 = doWork2();//p2接受的就是拷贝出的副本
}
构造函数调用规则
默认编译器会给一个类添加三个函数:默认构造空函数,默认析构空函数和默认拷贝构造函数。
如果定义了有参构造函数,则编译器不再提供无参构造,但会提供默认拷贝构造函数。
如果定了拷贝构造函数,则编译器不再提供其他构造函数。
深拷贝与浅拷贝
浅拷贝:赋值拷贝操作
深拷贝:在堆区中重新申请空间进行拷贝
堆区数据由析构函数手动释放。
当类中有属性值存放在开辟的堆区时,当在调用完在析构函数中进行释放时,由于利用默认拷贝构造函数拷贝的对象,其两个对象的指针变量指向同一块堆区,两个对象结束前都会调用各自的析构函数。在释放时会出现之后的一个指针变量指向的堆区在被释放时其实已经被之前的指针变量释放过了,就会导致在释放该堆区时造成重复释放出错,这就是浅拷贝。
解决方法:不使用编译器提供的默认拷贝构造函数,在自己写的拷贝构造函数中重新开辟新的堆区空间,就能使得两个对象正确释放各自的堆区。
实例代码:
class Person
{
public:
/*Person(const Person& p) {
}*/
Person(int age, int height) {
this->age = age;
this->height = new int(height);//身高开辟在堆区,由程序员手动分配、
//手动释放
}
int age;
int* height;
//堆区开辟的数据需要在对象销毁前进行释放,因此适合在析构函数中进行该操作
~Person() {
if (height != NULL) {//在析构函数中释放开辟的堆区数据
delete height;
height = NULL;//防止变成野指针
}
}
//自己写拷贝构造函数,不用默认的,来解决浅拷贝的问题
Person(const Person& p) {
this->age = p.age;
//this->height = p.height;编译器提供的默认拷贝构造就是这样的
this->height = new int(*p.height);//深拷贝操作,重新开堆区
}
};
void test() {
Person p1(10,160);
Person p2(p1);//调用自己写的拷贝构造函数
cout << "p2's age = " << p2.age << "p2's height = "<< p2.height<<endl;
原理如图:
初始化列表:对属性初始化
语法:构造函数():属性1(值1),属性2(值2)…{}
class Person
{
public:
Person(int a, int b, int c) :m_a(a), m_b(b), m_c(c) {}
int m_a;
int m_b;
int m_c;
};
int main() {
Person p(10,20,30);
return 0;
}
类对象作为类成员
描述:一个类的对象是另一个类的成员,叫对象成员。
class Phone {
public:
string pName;
};
class Person
{
public:
Person(string name, string pName) {
this->name = name;
this->phone.pName = pName;
}
string name;
Phone phone;
};
void test2() {
Person p("张三", "苹果");
cout << p.name << "使用的是" << p.phone.pName << endl;
}
注意:当其它类对象作为本类成员,构造时先构造其他类对象,再构造本类,即先调用其他类的构造函数,再调用本类构造函数;析构的顺序相反。
静态成员变量和函数(static)
静态成员变量和函数共同点:都有两种访问方式:对象访问和类名访问;都服从作用域
静态成员变量特点:所有对象共享同一份数据;在编译阶段分配内存;类内声明,类外初始化。
特点如下图:
class Person {
public:
static int m_A;//类内声明
//静态成员变量也服从作用域
private:
static int m_B;
};
int Person::m_A = 10;//类外初始化
int Person::m_B = 20;
int main() {
Person p;
cout << p.m_A << endl;//p.m_A == 10
Person p2;
p2.m_A = 20;
cout << p.m_A << endl;//p.m_A == 20,体现静态变量被所有对象共享
//因此静态变量有两种访问方式:对象访问和类名访问
Person::m_A = 30;//通过类名访问静态变量,方式 类名::成员名
cout << p.m_A << endl;
//Person::m_B访问不到
return 0;
}
静态成员函数特点:所有对象共享同一个函数;静态成员函数只能访问静态成员变量。
class Person
{
public:
static void func() {
a = 10;
//b = 20;无法访问b,静态函数只能访问静态变量
}
static int a;//静态成员变量
int b;//非静态成员变量
};
int main() {
Person p1;
p1.func();//对象访问静态函数
Person::func();//类名访问静态函数
return 0;
}
静态成员函数、变量以及非静态成员函数都不属于某一个类对象,不算在该类所占的空间中。只有非静态成员变量才属于该类对象。
成员变量和成员函数分开存储,每一个非静态成员函数只会有一份函数实例,所有的对象共享这份函数。
那么如何区分该函数是为哪一个对象所调用的呢?就用到this指针,this指针指向被调用的成员函数所属的对象,就是说谁调用的函数,this指谁。this隐藏在每一个非静态成员函数中。
this用途:
1)当形参和成员变量名同名时,用this来区分;
这个跟Java一样,this->age = age;//这里C++必须用->
2)在类的非静态成员函数中,想返回对象本身,可用 return *this;
Person& addAge(Person &p1){//返回值的必须是Person&,如若不然返回得则是调用拷贝构造函数后的副本
this->age += p1.age;
retun *this;//this指向对象,因此*this返回对象本身
}
注意,如果以Person作为类型的函数,返回的都会是一个副本,只有用Person& 才会返回对象本身。
C++允许空指针调用成员函数
但要注意有没有用到this指针
class Person
{
public:
Person();
~Person();
void showClassName(){
cout << "showclassname" << endl;
}
void showAge() {
if(this == null){//空指针直接return,提高健壮性
return;
}
cout << "age =" << age << endl;//编译器对于age这样的写法,其实都会自动认为是this->age,
//由于p指针是空指针,this没有指代任何一个确切的对象,
//因此this是空的
}
int age;
};
void test() {
Person* p = NULL;
p->showClassName();//可以正常运行
p->showAge();//会报错
}
常函数和常对象
常函数:const加在成员函数前变为常函数,常函数内不能修改成员属性,但成员属性声明前加mutable,则依然可以被修改。
常对象:常对象只能调用常函数
//常函数
class Person {
public:
//this指针的本质是常量指针,因此指向是不能修改的
//Person * const this;//this本质是常量指针
//const Person * cponst this;//this变为指向值也不能修改的指针
void showPerson() const{//加上的const是修饰该函数的隐藏指针this的
//使得this指向的值也不能修改
this->m_a = 100;
this->m_b = 100;//加上mutable后的变量常函数也能改
}
int m_a;
mutable int m_b;
};
void test() {
const Person p;//常对象
//p.m_a = 10;//错误语句,常对象的属性不能修改
p.m_b = 10;//正确,加了mutable的属性则可以修改
p.showPerson();//正确,常对象只能调用常函数
}
友元
友元(friend)就是让某些函数或者类也能访问到私有成员(private)。
三种实现:全局函数作友元、类做友元和成员函数做友元。
1)全局函数作友元
class Building {
friend void goodGay(Building* building);//这表示goodGay这个函数是Building类的一个友元,那么goodGay可以访问Building类的私有成员
public:
Building() {
m_SittingRoom = "客厅";
m_SittingRoom = "卧室";
}
string m_SittingRoom;
private:
string m_LivingRoom;
};
//全局函数
void goodGay(Building *building) {
cout << "全局函数正在访问:" << building->m_SittingRoom << endl;
cout << "全局函数正在访问:" << building->m_LivingRoom<< endl;
}
2)类作为友元
class Building {
friend class GoodGay;//GoodGay这个类是Building类的友元
//那么GoodGay这个类便可以访问Building类的private成员
public:
Building();
string m_SitingRoom;
private:
string m_LivingRoom;
};
3)(别的类的)成员函数作为友元
class Building {
friend void GoodGay::visit();//GoodGay中的visit成员函数作为了友元
//那么GoodGay类中的visit()函数可以访问Building类中的private成员
public:
Building() {
m_SittingRoom = "客厅";
m_LivingRoom = "卧室";
}
string m_SittingRoom;
private:
string m_LivingRoom;
};
5、运算符重载
实现自定义类型的相互运算。
1)加号
成员函数重载加号
class Person {
public:
Person(){}
Person(int a, int b) {
m_A = a;
m_B = b;
}
//成员函数重载加号
Person operator+ (Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
int m_A;
int m_B;
};
Person p1(10,10);
Person p2(10,10);
Person p3 = p1.operator+(p2);//本质调用,可简化如下
Person p4 = p1 + p2;//简化版,直接使用加运算符
全局函数重载加号运算符
//全局函数重载加号
Person operator+ (Person& p1, Person& p2) {
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
Person p1(10,10);
Person p2(10,10);
Person p3 = operator+(p1,p2);//本质调用,可简化如下
Person p4 = p1 + p2;//简化版,直接使用加运算符
运算符重载也可以发生函数重载。
2)左移运算符:<<
作用:配合友元可以实现输出自定义数据类型
//利用成员函数重载左移运算符 p.operator(cout) ->p << cout ,与想实现的不对
//因此不会再成员函数实现左移运算符重载
/*void operator<<(cout) {
}*/
全局函数实现:
class Person {
//属性设为私有的情况下,使用全局函数的友元让全局函数访问私有属性
friend ostream& operator<<(ostream& cout, Person& p);
public:
//利用成员函数重载左移运算符 p.operator(cout) ->p << cout ,与想实现的不对
//因此不会再成员函数实现左移运算符重载
/*void operator<<(cout) {
}*/
Person(int a, int b) {
m_a = a;
m_b = b;
}
private://
int m_a;
int m_b;
};
//只能利用全局函数重载左移运算符
ostream& operator<<(ostream &cout,Person &p) {
cout << "m_a = " << p.m_a << "m_b = " << p.m_b ;
return cout;//返回cout类型满足链式编程思想
}
void test() {
Person p(10,10);
cout << p<<"链式编程"<<endl;
}
3)递增运算符
class MyInteger {
friend ostream& operator<<(ostream& cout, MyInteger& mInt);
public:
MyInteger() {
m_Num = 0;
}
//重载前置++运算符
MyInteger& operator++() {//返回引用是为了一直对一个数据操作
//返回类型是引用
m_Num++;
return *this;
}
//重载后置++运算符
MyInteger operator++(int) {//int代表占位参数,可区分前、 后置
//返回类型是值
//先记录当时结果
MyInteger temp = *this;
//递增
m_Num++;
//将记录结果返回
return temp;
}
private:
int m_Num;
};
//重载<<运算符
ostream& operator<<(ostream& cout, MyInteger& mInt) {
cout << mInt.m_Num;
return cout;
}
4)赋值运算符重载
C++编译器给一个类至少会提供4个函数,除了之前说的三个,还会提供一个operator=对属性进行拷贝。
class Person {
public:
Person(int age) {
m_Age = new int(age);//开辟在堆区
}
//重载赋值运算符
void operator=(Person& p) {
//编译器提供浅拷贝 m_Age = p.m_Age;
//应该先判断是否有属性在堆区,如果有先释放干净,再深拷贝
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age);//深拷贝
//return *this;//返回自身,实现链式赋值
}
~Person() {
if (m_Age != NULL)//释放堆区数据
{
delete m_Age;
m_Age = NULL;
}
}
int* m_Age;
};
6、继承和多态
继承
语法:class 子类名:继承方式 父类名{};
继承方式三种:
公共继承(public)、私有继承(private)和保护继承(protected)
总结:公共继承访问方式不变;保护继承保护访问,私有继承私有访问;私有成员会被编译器隐藏,但依然会被继承;静态成员则不能被继承。
继承中的构造和析构的顺序
创建子类对象,也会创建父类对象,
函数调用顺序:父类构造 -> 子类构造 -> 子类析构 -> 父类析构。栈的方式。
继承同名成员处理方式
访问子类同名成员:.出来就可,s.m_a;
访问父类同名成员:说明作用域,s.Base::m_a;
如果子类中出现与父类同名的函数,则子类的同名函数会隐藏掉父类所有的同名函数,包括父类中重载的同名函数。
继承同名静态成员处理方式
静态成员和非静态成员处理方式一样。
多继承语法
class 子类:继承方式 父类1,继承方式 父类2…
多继承可能引发父类中有同名成员的现象,要加作用域区分。
实际编程不建议使用多继承。
菱形继承
两个派生类继承同一个类,这两个类又被同一个继承。
菱形继承问题:两个父类具有相同成员时,子类调用时用作用域加以区分。
菱形继承导致同一数据有两份,资源浪费。
利用虚继承可以解决菱形继承问题
class 子类:virtual 继承类型 父类名{};//虚继承,父类成为虚基类
多态
两类:静态多态:函数重载和运算符重载;动态多态:派生类和虚函数实现运行时多态。
区别:静态多态的函数地址早绑定,编译阶段确定函数地址,动态多态晚绑定,运行阶段确定函数地址。
地址早绑定:
class Animal
{
public:
//Animal();
//~Animal();
void speak() {//虚函数实现晚绑定,运行时确定函数地址
cout << "动物在说话" << endl;
}
private:
};
class Cat :public Animal {
public:
void speak() {
cout << "喵喵喵" << endl;
}
};
void doSpeak(Animal &animal) {//Animal &animal = cat,父类引用指向子类对象
animal.speak();
}
void test() {
Cat cat;
doSpeak(cat);
}
此时,执行的结果为:
即使给父类引用传递的是cat,依然会执行父类的speak()函数,这就是因为地址早绑定了
动态多态、地址晚绑定实现条件:
1)有继承关系 2)子类要重写父类的虚函数
ps:重写:子类函数名、返回值类型、参数列表完全与父类相同。
class Animal
{
public:
//Animal();
//~Animal();
virtual void speak() {//虚函数实现晚绑定,运行时确定函数地址
cout << "动物在说话" << endl;
}
private:
};
class Dog :public Animal {
public:
void speak() {
cout << "汪汪汪" << endl;
}
};
class Cat :public Animal {
public:
void speak() {
cout << "喵喵喵" << endl;
}
};
void doSpeak(Animal &animal) {//Animal &animal = cat
animal.speak();
}
动态多态的使用:父类的指针或引用指向子类对象
这一步:
void doSpeak(Animal &animal) {//Animal &animal = cat,父类引用指向子类对象
animal.speak();
}
void test() {
Cat cat;
doSpeak(cat);//子类对象传递给父类的引用
Dog dog;
doSpeak(dog);
}
运行结果如下:
虚函数实现动态多态的原理
多态计算器例子:
class Calculator {
public:
int getResult(string oper) {
if (oper == "+") {
return num1 + num2;
}
else if (oper == "-") {
return num1 - num2;
}
else if(oper == "*")
{
return num1 * num2;
}
}
//如果扩展新功能,需要修改代码
//实际开发中,提倡开闭原则,对扩展开放,对修改关闭
int num1;
int num2;
};
//利用多态实现计算器
//实现计算器抽象类
class AbstrctCalculator {
public:
virtual int getResult() {
return 0;
}
int num1;
int num2;
};
//加法计算器类
class AddCalculator :public AbstrctCalculator {
public:
int getResult() {
return num1 + num2;
}
};
//减法计算器
class SubCalculator :public AbstrctCalculator {
public:
int getResult() {
return num1 - num2;
}
};
//乘法计算器
class MulCalculator :public AbstrctCalculator {
public:
int getResult() {
return num1 * num2;
}
};
void test2() {
AbstrctCalculator* abc = new AddCalculator;//父类指针指向子类对象,发生多态
abc->num1 = 10;
abc->num2 = 20;
cout << abc->num1 << "+" << abc->num2 << "=" << abc->getResult() << endl;
delete abc;//用完销毁,释放堆区数据。但指针还在
abc = new SubCalculator;//减法
abc->num1 = 10;
abc->num2 = 20;
cout << abc->num1 << "-" << abc->num2 << "=" << abc->getResult() << endl;
delete abc;
}
多态好处:1)组织结构清晰;2)可读性强;3)对后期维护和扩展很友好
纯虚函数和抽象类
多态中,通常父类中的虚函数是毫无意义的,主要都是调用的子类重写的内容,因此可以将虚函数改为纯虚函数。
语法: virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,该类也就被称作抽象类
抽象类特点:无法实例化对象;子类中必须重写父类里的纯虚函数,否则也属于抽象类。
class Base {//有一个纯虚函数,该类就是抽象类
public:
virtual void func() = 0;//纯虚函数
//抽象类的子类必须重写父类的纯虚函数,否则也属于抽象类
};
class Son :public Base {
public:
void func() {
cout << "func函数调用" << endl;
}
};
void test1() {
//Base b;栈上
//new Base;堆上
//抽象类无法实例化对象
Base* base = new Son;
base->func();
}
多态案例—冲饮品
class AbstractDrinking {
public:
//煮水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//制作饮品
void makeDrinking(){
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee :public AbstractDrinking {
public:
//煮水
virtual void Boil() {
cout << "煮水" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "倒入杯中" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入糖喝牛奶" << endl;
}
};
//制作茶叶
class Tea :public AbstractDrinking {
public:
//煮水
virtual void Boil() {
cout << "煮开水" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "倒入紫砂杯" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加柠檬枸杞" << endl;
}
};
//制作函数
void doWork(AbstractDrinking * abc) {
abc->makeDrinking();
delete abc;//释放堆区数据
}
void test1() {
//制作咖啡
doWork(new Coffee);//实现多态
}
void test2() {
//制作茶水
doWork(new Tea);//实现多态
}
虚析构和纯虚析构
作用:在使用多态时,父类指针指向子类成员,当子类中有属性开辟到堆区时,父类指针在释放(delete)时无法调用子类的析构代码,就会导致堆区数据得不到释放,造成内存泄漏等问题。
虚析构与纯虚析构的共性:
都可以解决父类指针释放子类对象
都需要具体的函数实现
区别:
有纯虚析构的类属于抽象类,无法实例化对象。
语法:
虚析构:virtual ~类名(参数列表){}
纯虚析构:virtual ~类名(参数列表)= 0;
类名::~类名(){}//纯虚析构的实现
class Animal
{
public:
Animal() {
cout << "animal构造函数调用" << endl;
}
~Animal() {
cout << "animal析构函数调用" << endl;
}
virtual void speak() = 0;//纯虚函数
/*{
cout << "动物在说话" << endl;
}*/
private:
};
class Cat :public Animal {
public:
Cat(string name) {
cout << "cat构造函数调用" << endl;
m_name = new string(name);
};
void speak() {
cout <<*m_name<< "喵喵喵" << endl;
}
~Cat() {
if (m_name != NULL)
{
cout << "cat析构函数调用" << endl;
delete m_name;
m_name = NULL;//避免成为野指针
}
}
string* m_name;//存放在堆区
};
void test() {
Animal* animal = new Cat("Tom");//使用指针完成多态
animal->speak();
//父类指针在析构时,不会调用子类的析构函数
delete animal;//会调用animal的析构函数
//Cat cat;
//doSpeak();
}
执行结果如下:
可以看到并未执行Cat类的析构函数,这就是因为父类指针无法调用子类的析构代码,会导致内存泄漏的问题。
解决办法:父类析构函数改为虚析构函数。
virtual ~Animal() {//虚析构解决子类对象释放堆区数据不干净的问题
cout << "animal析构函数调用" << endl;
}
结果如下:
纯虚析构:
virtual ~Animal() = 0;//纯虚析构
纯虚析构函数必须有实现,因为如果父类有堆区数据,则释放的过程就需要由这个纯虚函数执行,实现方式如下:
Animal::~Animal() {//纯虚析构函数的实现
}
纯虚析构既要声明,还要实现(与纯虚函数相比)。
有纯虚析构的类也是抽象类。
7、文件操作
对文件操作需要包含头文件< fstream > 文件流
文件类型分为文本文件和二进制文件 。
写文件:
创建流对象:ofstream ofs;
打开文件:ofs.open(“文件路径”,打开方式);
写数据:ofs <<“写入的数据”;
关闭文件:ofs.close();
打开方式:
打开方式可以配合使用,用 | 操作符连接。
读文件
创建流对象:ifstream ifs;
打开文件并判断是否打开成功:ifs.open(“文件路径”,打开方式);
读数据:四种读取方式。
关闭文件:ifs.close();
四种读文件方式
//第一种
char buf[1024] = { 0 };
while (ifs >> buf) {
cout << buf << endl;
}
//第二种
char buf[1024] = { 0 };
while (ifs.getline(buf,sizeof(buf)))
{
cout << buf << endl;
}
//第三种
string buf;
while (getline(ifs,buf))
{
cout << buf << endl;
}
//第四种,不太推荐,一个一个字符得读
char c;
while ((c = ifs.get()) != EOF) {
cout << c << endl;
}
二进制文件的读写先放一放。