主要针对C++面向对象编程技术做详细介绍,探讨C++中的核心和精髓。
这里写目录标题
- 一 内存分区模型
- 二 引用
- 三 函数提高
- 四 类和对象
- 4.1 封装
- 4.2 对象的初始化和清理
- 4.2.1 构造函数和析构函数
- 4.2.2 构造函数的分类及调用
- 4.2.3 拷贝构造函数调用时机
- 4.2.4 构造函数调用规则
- 面试:4.2.5 深拷贝与浅拷贝
- 4.2.6 初始化列表
- 4.2.7 类对象作为类成员
- 4.2.8 静态成员 :在成员变量和成员函数前加上关键字static,称为静态成员、
- 4.3 C++对象模型和this指针
- 4.4 友元
- 4.5 运算符重载 (基本是对自己新声明的类型进行运算符重载)
- 4.5.1 加号运算符重载。
- 4.5.2 左移 运算符重载。
- 4.5.3 递增 运算符重载
- 4.5.4 赋值 运算符重载
- 4.5.5 关系 运算符重载 主要是两个 == 和 !=
- 4.5.5 函数调用 运算符重载 ()
- 4.6 继承
- 4.6.1 公有继承:
- 4.6.2 保护继承:
- 4.6.3 私有继承:
- 4.6.4 继承中的对象模型
- 4.6.5 继承中构造和析构顺序
- 4.6.6 继承中同名成员处理方式
- 4.6.7 继承同名静态成员处理方式
- 4.6.8 多继承语法
- 4.6.9 菱形继承
- 4.7 多态
- 五 文件操作
一 内存分区模型
C++ 程序在执行时,将内存大方向划分为 4个区域
内存四区意义:不同区域存放的数据,赋予不同的声明周期,给我们更大的灵活编程
1.代码区:存放函数体的二进制代码,由操作系统进行管理的
2.全局区:存放全局变量和静态变量以及常量
3.栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
4.堆区:有程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
1.1 程序运行前 分为如下两个区,特点是共享和只读
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
1.存放CPU执行的机器指令
2.代码区是 共享 的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
3.代码区是 只读 的,使其只读的原因使防止程序意外地修改了它地指令。
全局区:
1.全局变量 和 静态变量存放在此。
2.全局区还包含了常量区,字符串常量和其他常量也存放在此。
const修饰的局部变量的确是常量 但并不在全局区
局部变量不是存放在全局区的 哪怕是const修饰的常量
3.该区域地数据在程序结束后由操作系统释放。
全局区存放的为全局变量,静态变量和常量
常量区存放 const修饰的全局常量 和 字符串常量
1.2 程序运行后
栈区:
由编译器自动分配释放,存放函数的参数值(形参也在栈区),局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
返回一个局部变量的地址,编译器报了错,的确在打印第二次的时候出现了乱码。
#include <iostream>
using namespace std;
int* func()
{
int a = 10;
return &a;
}
int main()
{
int* p = func();
cout << *p << endl;
cout << *p << endl;
}
结果:
第一次可以打印正确的数字,是因为编译器做了保留
第二次这个数据就不再保留了
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new在堆区开辟内存
1.3 new delete操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
语法 new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针。
堆区的数据 由程序员管理开辟,程序员管理释放
如果想释放堆区的数据,利用关键字delete
内存被释放后 再次访问就是非法操作 会报错
二 引用
给变量起别名
语法:数据类型 &别名 = 原名
2.1 引用注意事项
1.引用必须初始化。
2.引用一旦初始化后,就不可以更改了。
2.2 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
2.3 引用做函数返回值
1.不要返回局部变量的引用
2.函数的调用可以作为左值。
int& func()
{
static int a = 10;
return a;
}
int main()
{
func() = 100; 相当于a = 100;
}
2.4 引用的本质 (在C++内部实现是一个指针常量)
引用一旦初始化后,就不可以发生改变。
2.5 常量引用 作用:常量引用主要用来修饰形参,防止误操作。
三 函数提高
3.1 函数默认形参: c++中,函数的形参可以有默认值的;
语法:返回值类型 函数名 (参数 = 默认值) { }
int func(int a,int b=20,int c =30){}
//如果我们自己传入数据,就用自己的数据,如果没有,那么用默认值
//语法:返回值类型 函数名(形参 = 默认值){}
注意事项:
1.如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值。
2…如果函数声明有默认参数,函数实现就不能有默认参数。
// 声明和实现 只能有一个默认参数;
3.2 函数占位参数
c++中函数的形参列表里可以有占位参数,用来做占位。调用函数时必须填补该位置。
语法:返回值类型 函数名 (数据类型){}
void func(int a,int){} //调用必须填补
注意:占位参数 还可以有默认参数
void func(int a,int = 10){}
3.2 函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
同一个作用域下
函数名称相同
函数参数类型不同 或者 个数不同 或者 顺序不同
注意:函数的返回值不可以作为函数重载的条件
3.2.1 函数重载注意事项
①.引用作为重载条件
void func(int &a){} //传可读可写的变量
void func(const int &a){} //传常量 只读 不可修改
②.函数重载碰到默认参数
void func(int a,int b = 10){} //默认参数
void func(int a){}
如上两函数存在时,调用时会出现二义性,报错,尽量避免这种情况;
不写默认参数最好。
四 类和对象
c++面向对象的三大特性为:封装,继承,多态。
c++认为万事万物皆为对象,对象上有其属性和行为。
4.1 封装
4.1.1 封装的意义
将属性和行为作为一个整体,表现生活中的事物
将属性和行为加以权限控制
封装意义一: 在设计类的时候,属性和行为写在一起,表现事物。
语法:calss 类名 { 访问权限:属性 / 行为 };
权限:public,protected,private
属性:类中定义的变量
行为:函数
什么是实例化 : 通过一个类 创建一个对象的过程
封装意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制
权限有三种:
public 公共权限:成员 类内可以访问,类外可以访问
protected 保护权限:成员 类内可以访问 类外不可以访问 (儿子可以访问父亲中的保护内容)
private 私有权限 成员 类内可以访问 类外不可以访问 (儿子不可以访问)
4.1.2 struct 和 class 区别
唯一的区别在于:默认的访问权限不同
struct 默认权限是 公有 pubulic
class 默认权限是 私有 private
4.1.3 成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
class Person
{
public:
//设置姓名 可读
void setName(string name)
{
m_name = name;
}
//获取姓名 可写
string getName()
{
return m_name;
}
//获取年龄 只读
int getAge()
{
m_age = 0;
return m_age;
}
//获取情人 只写
void setLover(string lover)
{
m_lover = lover;
}
private:
// 设置权限 姓名 可读可写
string m_name;
// 设置权限 年龄 只读
int m_age;
// 设置权限 情人 只写
string m_lover;
}
4.2 对象的初始化和清理
4.2.1 构造函数和析构函数
这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
构造函数:
主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:
主要作用于对象销毁前系统自动调用,执行一些清理工作
构造函数语法: 类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同,
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。
析构函数语法:~类名(){}
1.析构函数,没有返回值也不写void
2.函数名称与类名相同,在名称前加上符号~
3.析构函数不可以有参数,因此不可以发生重载。
4.程序在对象销毁前自动调用析构,无须手动调用,而且只会调用一次。
4.2.2 构造函数的分类及调用
两种分类方式:
按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造
拷贝构造函数
class Person
{
public:
int age;
//将传入的人身上的所有属性,拷贝到我身上
Person(const Person &p)
{
age = p.ger;
}
}
三种调用方式:
括号法
显示法
隐式转换法
//1.括号法 (最简单,舒服)
Person p1; //默认构造函数调用
Person p2(10); //有参构造函数
Person p2(p1); //拷贝构造函数
注意事项:调用默认构造函数时候,不要加();
//2.显示法
Person p1;
Person p2 = Person(10); //有参构造
Person p3 = Person(p2); //拷贝构造
注意事项 1: Person(10); //匿名对象,特点:当前行执行结束后,系统会立即回收掉匿名对象。
注意事项2:不要用拷贝构造函数,初始化匿名对象。
Person(p3);//编译器会认为 这是一个变量声明,与上重复了。发生重定义错误。
//3.隐式转换法
Person p4 = 10; //相当于 写了 Person p4 = Person(10);
Person p5 = p4; 调用了拷贝构造函数。
4.2.3 拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
声明:创建一个类方便演示
class Person
{
pubulic:
Person(){cout<<"调用无参构造函数\n";}
Person(int a){cout<<"调用有参构造函数\n";}
Person(const Person& p){cout<<"调用拷贝构造函数\n";}
~Person(){cout<<"调用析构函数\n";}
};
① 使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
Person p1(20);
Person p2(p1);
}
② 值传递的方式给函数参数传值
void doWork(Person p)
{
}
void test02()
{
Person p;
dowork(p);
}
③ 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int*)&p1 << endl; //打印地址做测试
return p1;
}
void test03()
{
Person p = doWork2();
cout << (int*)&p1 << endl; //打印地址做测试 检验上下地址是否相同
}
main函数
int main()
{
test01();//测试1
test02();//测试2
test03();//测试3
}
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行拷贝(值拷贝)
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造。
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数。
面试:4.2.5 深拷贝与浅拷贝
什么是深拷贝,浅拷贝 ? 什么时候用析构函数 ?
浅拷贝:简单的赋值拷贝操作 , 编译器提供的拷贝都是浅拷贝。
深拷贝:在堆区重新申请空间,进行拷贝操作。
深拷贝测试代码:(有错)
class Person
{
public:
int m_a;年龄
int *m_height;身高
Person(){cout<<"调用无参构造函数\n";}
Person(int a,int height)
{
m_a=a;
m_height = new int(height); //手动申请需要手动释放
cout<<"调用有参构造函数\n";
}
~Person()
{
//析构代码,将堆区开辟数据做释放操作
if(m_height!=nullptr)
{
delete m_height;
m_height = nullptr;
}
cout<<"调用析构函数\n";
}
};
void test01()
{
Person p1(18,160);
cout << "p1的年龄为:" << *p1.m_a << endl;
Person p2(p1); //编译器提供浅拷贝构造函数为p2赋值p1的属性
cout << "p2的年龄为:" << *p2.m_a << endl;
}
int main()
{
test01();
return 0;
}
如上测试 能输出 但是抛出异常,说明代码有问题
利用编译器提供的拷贝构造函数,会做浅拷贝操作。
浅拷贝带来的问题就是堆区的内存重复释放
如何解决:浅拷贝的问题,利用深拷贝进行解决。
//自己实现拷贝构造函数,解决就行了
Person(const Person &p) //类中添加就行了
{
cout << "Person 拷贝构造函数调用" << endl;
m_a = p.m_a;
//m_height = p.m_height;//编译器默认实现就是这行代码
m_height = new int(*p.m_height); //深拷贝
}
4.2.6 初始化列表
C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2) … {}
4.2.7 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员 (组合)
4.2.8 静态成员 :在成员变量和成员函数前加上关键字static,称为静态成员、
访问:可以通过对象访问;可以通过类名访问;
- 静态成员变量
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化 - 静态成员函数 (类外访问不到私有静态成员函数)
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储。
只有非静态成员变量才属于类的对象上——重要
class Person
{
public:
static int m_B;//静态成员变量,不属于类对象上
};
int Person::m_B = 0;//类内声明,类外初始化
int main()
{
Person p;
}
//空对象占用内存空间为1
//C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置
//每个空对象也应该有一个独一无二的地址
4.3.2 this指针概念
C++成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象共用一块代码,那么问题是:这一块代码如何区分哪个对象调用自己呢?
C++提供 this指针解决上述问题, this指针指向被调用的成员函数所属的对象。
this指针是隐含每一个非静态成员函数内的一种指针。
this指针不需要定义,直接使用即可。
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分 // 1. 解决名称冲突
- 在类的非静态成员函数中返回对象本身,可使用return *this // 2. 返回对象本身 *this
4.3.3 空指针访问成员函数
C++中空指针可以调用成员函数,但是要注意有没有用到this指针,
如果用到this指针,需要加以判断保证代码的健壮性。
class Person
{
public:
void showPerson()
{ //加此判断防止 无对象访问函数
if(this == nullptr)
{
return;
} //使用this时表示当前对象,而空指针访问时无对象,会报错。
cout << "age = " << this->m_a << endl;
}
int m_a;
};
int main()
{
Person *p =nullptr;
p->showPerson();
}
4.3.4 const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数。
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改。
常对象: - 声明对象前加以const称该对象为常对象
- 常对象只能调用常函数
- 添加mutable后可以修改常对象。
class Person
{
public:
int m_a;
void showPerson() const
{
//this指针的本质时 指针常量 指针的指向是不可以修改的
// const Person* const this:
// 在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
//this->m_a = 100; 普通成员变量不可修改
this->m_b = 100; //特殊值可以修改
}
int m_a; //特殊变量,即使在常函数中,也可以修改这个值
mutable int m_b;
};
void test01()
{
Person p;
p.showPerson();
}
int main()
{
test01();
return 0;
}
4.4 友元
在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的:让一个函数或者类 访问另一个类中私有成员
友元的关键字为:friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
将函数的声明放在类里,且添加friend关键字,这样就可以访问类中私有成员了。
4.4.2 类做友元 ( 友元类 )
将一个类在另一个类的开头声明,并添加friend关键字
4.4.3 成员函数做友元 (讲的代码自己测试错误,所以就暂时停止)
4.5 运算符重载 (基本是对自己新声明的类型进行运算符重载)
运算符重载概念:对已有的运算符冲i性能进行定义,赋予其另一种功能,以适应不同的数据类型。
4.5.1 加号运算符重载。
作用:实现两个自定义数据类型相加的运算。
//对于内置数据类型,编译器知道如何进行运算
总结1:对于内置的数据类型的表达式的运算符是不可能改变的
总结2:不要滥用运算符重载
#include <iostream>
using namespace std;
//通过自己写成员函数,实现两个对象相加属性后返回新的对象
//加号运算符重载
//1.通过成员函数重载+号
//2.通过全局函数重载+
class Person
{
public:
//1.通过成员函数重载+号
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;
};
//2.通过全局函数重载+
/*
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;
}
*/
void test01()
{
Person p1;
p1.m_a = 10;
p1.m_b = 5;
Person p2;
p2.m_a = 20;
p2.m_b = 5;
//Person p3 = p1.operator+(p2); //成员函数重载本质调用
//Person p3 = operator+(p1, p2);//全局函数重载本质调用;
Person p3 = p1 + p2; //运算符重载 也可以发生函数重载
cout << p3.m_a << endl;
cout << p3.m_b << endl;
}
int main()
{
test01();
return 0;
}
4.5.2 左移 运算符重载。
作用:可以输出自定义数据类型
总结:重载左移运算符配合友元可以实现输出自定义数据类型。
#include <iostream>
using namespace std;
//通常运算符重载有两种方式
//1.利用成员函数重载运算符 2.利用全局函数重载运算符
//但第一种方式并不适合左移运算符,因为无法实现 cout 在左侧,达到理想格式
//只能利用全局函数重载左移运算符 并且友元
//能达到什么要求呢, 知道其简化版本和原版本
//建议:写类的时候将属性私有化
//而属性私有化后,全局函数就必须变成友元函数才能访问私有属性
//所以通常我们看到的都是 友元全局重载左移运算符函数
class Person
{
friend ostream& operator<<(ostream& out, Person& p);
public:
//如果利用成员函数重载左移运算符
Person(int a, int b) :m_a(a), m_b(b) {}
int m_a;
int m_b;
};
ostream& operator<<(ostream& out, Person& p)
{
out << "m_a=" << p.m_a << " m_b=" << p.m_b << endl;
cout << "调用重载函数" << endl;
return out;
}
void test01()
{
Person p(10, 10);
p.m_a = 10;
p.m_b = 10;
cout << p << endl;
}
int main()
{
test01();
}
4.5.3 递增 运算符重载
作用:通过重载递增运算符,实现自己的整型数据。
递增运算符重载 ++,
先了解内置运算符, 前++ 和 后++
所以要写 前置递增 和 后置递增 两者的过程不同。
#include <iostream>
#include <string>
using namespace std;
class Test
{
friend ostream& operator<<(ostream& out, Test t);
public:
//重载前置++运算符
Test& operator++() //返回引用是为了对一个数进行操作
{
m_num++;
return *this;//把自身做一个返回
}
//重载后置++运算符
//int 代表占位参数,可以用于区分前置和后置递增
Test operator++(int)//后置递增返回 值
{
//先 记录当时的结果
Test temp = *this;
//后递增
m_num++;
//最后将记录结果做返回
return temp;//局部对象,不返回引用
}
private:
int m_num=10;
};
//重载<<运算符
ostream &operator<<(ostream& out, Test t)
{
out << "t.m_num=" <<t.m_num << endl;
cout << "调用左移重载运算符函数" << endl;
return out;
}
//前置测试
void test01()
{
Test t1;
cout << t1 << endl;
cout << ++t1 << endl;
}
//后置测试
void test02()
{
Test t2;
cout << t2++ << endl;
cout << t2 << endl;
}
int main()
{
//test01();
test02();
}
4.5.4 赋值 运算符重载
C++编译器一共会给一个类添加 4个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
4.赋值运算符operator=对属性进行值拷贝
重载 = 运算符
#include <iostream>
using namespace std;
class Person
{
public:
int *m_a;
Person(int a)
{
m_a = new int(a);
}
~Person()
{
if (m_a != nullptr)
{
delete m_a;
m_a = nullptr;
}
}
Person& operator=(Person& p)//返回引用
{
//编译器提供的浅拷贝如下
//m_a = p.m_a;
//应该先判断是否有属性在堆区,如果有 先释放干净,然后再深拷贝
if (m_a != nullptr)
{
delete m_a;
m_a = nullptr;
}
//深拷贝
m_a = new int(*p.m_a);
return *this;
}
};
void test01()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1;//赋值操作
cout << "p1的年龄为" << *p1.m_a << endl;
cout << "p2的年龄为" << *p2.m_a << endl;
cout << "p3的年龄为" << *p3.m_a << endl;
}
int main()
{
test01();
}
4.5.5 关系 运算符重载 主要是两个 == 和 !=
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
#include <iostream>
using namespace std;
class Person
{
public:
Person(string name, int age)
{
m_name = name;
m_age = age;
}
//重载 == 号
bool operator==(Person& p)
{
if (this->m_name == p.m_name && this->m_age == p.m_age)
{
cout << "p1 和 p2 是相等的!" << endl;
return true;
}
else
{
cout << "p1 和 p2 是不相等的!" << endl;
return false;
}
}
bool operator!=(Person& p)
{
if (this->m_name == p.m_name && this->m_age == p.m_age)
{
cout << "p1 和 p2 是相等的!" << endl;
return false;
}
else
{
cout << "p1 和 p2 是不相等的!" << endl;
return true;
}
}
string m_name;
int m_age;
};
void test01()
{
Person p1("Tom", 18);
Person p2("Tom", 20);
if (p1 == p2)
{
cout << "相等" << endl;
}
else if (p1 != p2)
{
cout << "不相等" << endl;
}
}
int main()
{
test01();
}
4.5.5 函数调用 运算符重载 ()
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
#include <iostream>
#include <string>
using namespace std;
//函数调用运算符重载
//打印输出类
class Print
{
public:
//重载函数调用运算符
void operator()(string test)
{
cout << test << endl;
}
};
void test01()
{
Print p1;
p1("hello,world");//使用起来非常像函数,所以称为仿函数
}
//仿函数非常灵活,没有固定的写法
int main()
{
test01();
}
//不想创建对象也可以使用 匿名对象 Print()(“hello”);
4.6 继承
继承的语法:class 子类 : 继承方式 父类
继承方式一共有三种:公共继承 保护继承 私有继承
private 私有权限, 三种继承都不可访问,但是存在,可以通过 sizeof测试出。
(友元可以使用 私有权限 内容)
4.6.1 公有继承:
权限继承到派生类后,原权限属性不变;
4.6.2 保护继承:
父类 公共和保护 权限到 子类中都变成保护权限,
4.6.3 私有继承:
父类 公共和保护 权限到 子类中都变成私有权限,
4.6.4 继承中的对象模型
问题:从父类继承过来的成员,哪些属于子类对象中 ?
父类所有非静态成员属性都会被子类继承下去,私有也继承了,只是编译器隐藏了。
VS 可以通过 VS开发人员命令提示符
先跳转到文件所属盘
然后复制文件地址 粘贴到命令行 回车 输入dir 回车
输入命令:c1 /d1 reportSingleClassLayout类名 文件名(可以用Tab)
这也就可以看到对象分布图
4.6.5 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数。
问:父类和子类的构造和析构顺序是谁先谁后?
继承中,先调用父类构造函数,再调用子类
4.6.6 继承中同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
继承后,若出现同名成员,直接调用首先访问的是自身类的成员
如果子类中出现和父类同名的成员函数,编译器会隐藏掉父类中所有同名成员函数
如果想访问到父类中被隐藏的同名成员函数,需要加作用域
Son s;
s.father::func();
总结:
1.子类对象可以直接访问到子类中同名成员
2.子类对象加作用域可以访问到父类同名成员
3.当子类与父类有用同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。
4.6.7 继承同名静态成员处理方式
问题:继承中同名的静态成员再子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致。
-
访问子类同名成员 直接访问即可
-
访问父类同名成员 需要加作用域
//通过子类访问父类作用域下的属性
Son::Father::m_num;
//第一个双冒号代表 通过类名方式访问
//第二个双冒号代表 代表访问父类作用域
① 通过类名访问 ②通过对象访问
子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数
如果想访问父类中被隐藏同名成员,需要加作用域。
4.6.8 多继承语法
C++允许一个类继承多个类,C++实际开发中不建议用多继承
语法:class 子类:继承方式 父类1, 继承方式 父类2…
多继承可能会引发父类中有同名成员出现,需要加作用域区分。
同名成员,会出现二义性。
总结:多继承中如果父类中出现了同名情况,子类使用时候要加作用域。
4.6.9 菱形继承
菱形继承概念:
两个派生类继承同一个基类,又有某个类同时继承着两个派生类。
这种继承被称为菱形继承,或者钻石继承。
菱形继承的问题:
1.最下端继承相同数据时会产生二义性,此时调用需要加作用域,解决此问题。
2.如上问题延伸,其实需要一份,所以相同数据应该只继承一份才对。
所以如何解决这两个问题呢?
1 问题末尾已给出解决方案
2 问题 利用虚继承 解决菱形继承孙子类继承两份相同的数据导致资源浪费以及毫无意义的问题。
在继承之前 加上关键字 virtual 变为虚继承,上端父类就称为 虚基类
4.7 多态
4.7.1 多态的基本概念
多态分为两类:
- 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定,编译器阶段确定函数地址。
- 动态多态的函数地址晚绑定,运行阶段确定函数地址。
动态多态满足条件
1.有继承关系。
2.子类要重写父类的虚函数。
(重写, 函数返回值类型 函数名 参数列表 完全相同就是重写)
动态多态使用
1.父类的指针或者引用,指向子类对象。
空类的大小是 1
包含虚函数的空类大小是 4
多态发生机制简易解释:
4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
纯虚函数:
只要有一个纯虚函数,这个类称为抽象类;
抽象类特点:
1.无法实例化对象; (不能 new)
2.抽象类的子类 必须要重写父类中的传虚函数,否则也属于抽象类
3.函数调用建议 (父类的指针或者引用,指向子类对象。)
4.7.4 虚析构 和 纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构 和 纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构 和 纯虚析构区别: - 如果是纯虚析构,该类属于抽象类,无法实例化对象
父类指针在析构时候 不会调用子类中析构函数 ,导致子类如果有堆区属性,出现内存泄漏
利用虚析构可以解决 父类指针释放子类对象时不干净的问题
(析构函数只能有一个,但是无论写哪个都必须要实现)
虚析构语法:
virtual ~类名(){}
纯虚析构语法:(直接如下第一行写是声明,还需要第二行去实现它)
virtual ~类名() = 0;
抽象类类名::~抽象类类名(){}
注意:有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
五 文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++对文件操作需要包含 头文件
文件类型分为两种:
1.文本文件:文件以文本的ASCII码形式存储在计算机中
2.二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
1.ofstream 写操作
2.ifstream 读操作
3.fstream:读写操作
5.1 文本文件
5.1.1 写文件
写文件步骤如下:
1.头文件 #include
2.创建流对象 ofstream ofs;
3.打开文件 ofs.open(“文件路径”,打开方式);
4.写数据 ofs << “写入的数据”;
5.关闭文件 ofs.close();
总结
- 文件操作必须包含头文件 fstream
- 读文件可以利用ofstream,或者fstream类
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用<<可以向文件中写数据
- 操作完毕,要关闭文件
5.1.2 读文件
读文件与写文件步骤相似,但是读取方式相对于比较多
读文件步骤如下:
1.头文件 #include
2.创建流对象 ifstream ifs;
3.打开文件 ifs.open(“文件路径”,打开方式);
4.读数据 四种方式读取
5.关闭文件 ifs.close();
总结:
- 读文件可以利用 ifstream 或者 fstream 类
- 利用 is_open 函数可以判断文件是否打开成功
- close 关闭文件
#include <iostream>
#include <fstream>
using namespace std;
void test01()
{
//1.包含头文件
//2.创建流对象
ifstream ifs;
//3.打开文件 并且判断是否打开成功
ifs.open("test.txt",ios::in);//假设test.txt已经存在
if(!ifs.is_open())
{
cout << "文件打开失败" << endl;
}
//4.读数据 四种方法
//第一种
char buf[1024] = {0};
while(ifs >> buf)
{
cout << buf << endl;
}
//第二种
char buf[1024] = 0;
while(ifs.getline(buf,sizeof(buf))) //两个参数,1.char * 2.count
{
cout << buf << endl;
}
//第三种
string buf; //要加头文件 <string>
while(getline(ifs,buf))
{
cout << buf << endl;
}
//第四种 //不太推荐
char c;
while((c = ifs.get()) != EOF)//EOF end of file
{
cout << c;
}
//5.关闭文件
ifs.close();
}
5.2 二进制文件
打开方式指定为 ios::binary
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char* buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
举例中出现的 //写字符串最好不要用string //反正底层还是用C写的
是因为string封装了char,本质上没区别,char 是基本类型,内存空间的申请还是它,可以互换。
用string就可以调用string类的成员函数 个人比较看重这点
#include <iostream>
#include <fstream>
using namespace std;
//二进制 写文件
class Person
{
public:
//写字符串最好不要用string //反正底层还是用C写的
char m_name[64]; //姓名
int m_age;//年龄
};
void test01()
{
//1.包含头文件
//2.创建流对象
//ofstream ofs;
//3.打开文件 //也可以利用其构造函数 将2.3 合二为一
//ofs.open("person.txt",ios::out|ios::binary);
ofstream ofs("person.txt",ios::out|ios::binary);
//假设 person.txt 已存在
//4.写文件
Person p = {"张三",18};
ofs.write((const char *)&p,sizeof(Person));
//两个参数:数据是啥,有多大
//5.关闭文件
ofs.close();
}
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数 read
函数原型:istream& read(char *buffer,int len);
测试案例:
#include <iostream>
#include <fstream>
using namespace std;
//二进制读文件
class Person
{
public:
char m_name[64];//姓名
int m_age;//年龄
};
void test01()
{
//1.包含头文件
//2.创建流对象
ifstream ifs;
//3.打开文件 判断文件是否打开成功
ifs.open("Person.txt",ios::in|ios::binary);
if(!ifs.is_open())
{
cout << "打开文件失败" << endl;
}
//4.读文件
Person p;
ifs.read((char *)&p,sizeof(Person));
cout << "姓名" << p.m_name << " 年龄:" << p.m_age << endl;
//5.关闭文件
ifs.close();
}