C++核心编程:面向对象和模板
##程序运行的内存分配
C++程序在执行时,将内存划分为4大区域,在不同存放的数据赋予了不同的生命周期,方便编程。
1.在程序运行前分为全局区和代码区,代码区特点是共享(频繁执行的程序在内存中只有一份代码即可)和只读(防止程序意外修改指令),全局区存放全局变量,静态变量,常量。常量是const修饰的全局常量和字符串常量,该区域数据是由操作系统自动释放。
2.程序运行后:栈区存放函数的参数值,局部变量,在运行结束后编译器自动释放该区域内存不能返回局部变量的地址。new在堆区开辟内存空间,new之后返回地址空间,需手动开辟手动释放。利用new创建的数据会返回数据对应该类型的指针。
开辟int类型的地址空间
int* a = new int(10);
开辟数组
int* arr = new int[10];
##引用–给变量起别名
使用语法:int &b = a;
引用必须初始化,引用的本质是指针常量,指针常量指向不可以修改,指向的值可以修改。
使用:函数做左值必须返回引用。
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
//返回静态变量引用
int& test02() {
static int a = 20;
return a;
}
test02() = 1000;
常量引用:主要来修饰形参,防止误操作。
//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
//v += 10;
cout << v << endl;
}
##3.函数提高
函数的默认参数:在函数的形参列表中是可以有默认值的。如果在某个位置参数有默认值,那么从这个位置往后必须要要有默认值。
函数重载:函数名相同提高复用性
函数重载满足的条件:(1)在同一个作用域下(2)函数名相同(3)函数参数类型不同或者个数不同或者顺序不同。但是函数的返回值不可以作为函数重载的条件。
void func(double a ,int b)
{
cout << "func (double a ,int b)的调用!" << endl;
}
//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
// cout << "func (double a ,int b)的调用!" << endl;
//}
引用作为函数重载的注意事项
void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}
int a = 10;
func(a); //调用无const
func(10);//调用有const
##4类和对象
C++面向对象的三大特性:封装,继承,多态
C++认为万事万物皆为对象,对象上有其属性和行为
封装的意义:将属性和行为作为一个整体,表现生活中的事物。并且将属性和行为加以权限控制。
三种访问权限:
//公共权限 public 类内可以访问 类外可以访问
//保护权限 protected 类内可以访问 类外不可以访问
//私有权限 private 类内可以访问 类外不可以访问
其保护和私有在继承上有所区分,保护属性在public和protect继承下类内可以访问类外不可以访问。私有权限只能在本类中进行访问,私有属性在继承下为不可访问。
在C++中 struct和class唯一的区别就在于 默认的访问权限不同
区别:
- struct 默认权限为公共
- class 默认权限为私有
将成员属性设置为私有有利于自己控制读写权限,对于写权限我们可以检测数据的有效性。
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供****编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
5. 析构函数,没有返回值也不写void
6. 函数名称与类名相同,在名称前加上符号 ~
7. 析构函数不可以有参数,因此不可以发生重载
8. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
拷贝函数的调用时机:
1.使用一个已经创建完毕的对象来初始化一个新的对象
2.值传递的方式给参数传值
3.以值方式返回局部对象
拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
Person newman(man); //调用拷贝构造函数
Person p1 = p;//值传递的方式给函数参数赋值
浅拷贝和深拷贝
浅拷贝:简单的赋值操作
深拷贝:在堆区重新开辟内存空间,进行拷贝操作
class Person {
public:
//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);//深拷贝重新分配内存空间
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
public:
int m_age;
int* m_height;
};
注意:如果有属性在堆区开辟,一定要提供拷贝构造函数,防止浅拷贝带来的问题。
初始化列表给属性赋值
语法:构造函数():属性1(值1),属性2(值2)... {}
类对象作为类成员
类中的成员可以是另一个类的对象,我们称该成员为对象成员
class A {}
class B
{
A a;
}
静态成员:静态成员变量,静态成员函数
静态成员变量:所有对象共享同一份数据,在编译阶段分配内存,类内声明,内外初始化。
静态成员函数:所有对象共享同一个数据,静态成员函数访问静态成员变量
在C++中类内的成员变量和成员函数分开存储,只有非静态成员变量才属于对象上占一个内存空间进行区分。每一个非静态成员函数只会生成一份函数实例,但有多个函数对象调用的时候通过this指针去调用函数对象。this指针隐含每一个非静态成员函数内的一种指针。
this指针的使用:当形参和成员变量同名时,通过this指针来区分。当类的非静态成员返回中返回对象本事,可使用*this。
空指针可以调用成员函数。但是如果成员函数中使用到了this指针就不可以,this指针指向的是空,要求空指针去调用成员变量非法。
const:
-
成员函数后加const后我们称为这个函数为常函数
-
常函数内不可以修改成员属性
-
成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象: -
声明对象前加const称该对象为常对象
-
常对象只能调用常函数,调用普通的成员函数会间接修改值,编译器会报错。
友元:为了让一个函数或者类访问一个类中的私有成员
友元的三种实现:全局函数做友元,类做友元,成员函数做友元。
运算符重载:对已有的运算符重载进行定义,赋予其另一种可能,以适应不同的数据类型。
对+号进行重载
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
Person p1(10, 10);
Person p2(20, 20);
//成员函数方式
Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
对左移运算符重载,可以对自定义数据类型追加
//返回引用。链式编程追加结果更方便
ostream& operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}
重载赋值操作`
//重载赋值运算符
Person& operator=(Person &p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
//编译器提供的代码是浅拷贝
//m_Age = p.m_Age;
//提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);
//返回自身
return *this;
} `
关系运算符重载
**作用:**重载关系运算符,可以让两个自定义类型对象进行对比操作
bool operator==(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}
继承–减少代码的重复性
私有成员继承下来会被隐藏但是还会继承下来。父类继承子类后,当创建子类对象,也会调用父类的构造函数。父类的构造函数先进行,子类的构造后进行,析构函数正好相反,
当子类与父类具有相同的成员函数,子类会隐藏父亲中所有的同名成员函数。当想要访问父类中被隐藏的成员同名函数需要加父类的作用域
继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
多继承会导致菱形继承导致子类继承两份相同的数据会产生数据的二义性。导致浪费资源毫无意义,利用虚继承会产生菱形继承问题。
/继承前加virtual关键字后,变为虚继承 底层(继承是他的虚基类指针加上偏移量变成他指向的数据 )
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
多态主要分为两类:静态多态和动态多态
静态多态:函数重载,运算符重载–地址早绑定,编译阶段确认函数地址
动态多态:派生类和虚函数–动态多态的函数地址晚绑定–运行阶段确定函数地址
多态满足条件:
1.有继承关系
2.子类重写父类中的虚函数(原理:父类的虚函数指向重写后的函数,让接口多元化)
3.父类指针指向子类对象
//多态实现
//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public :
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
//加法计算器
class AddCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};
AbstractCalculator *abc = new AddCalculator;//父类指针调用子类对象
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; //堆区数据手动开辟手动释放
在多态中,通常父类中的虚函数实现是毫无意义的,主要是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数的语法virtual 返回值类型 函数名 (参数列表)= 0 ;`
抽象类的特点:无法实列化对象,子类必须重写父类中的纯虚函数,否则也属于抽象类。
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
虚析构函数的作用:
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//怎么解决?给基类增加一个虚析构函数
//虚析构函数就是用来解决通过父类指针释放子类对象
5.文件操作
控制文件I/O流操作,操作文件分为三大类:ofstream:写操作 ifstream: 读操作 fstream : 读写操作
5.1.1写文件
写文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ofstream ofs;
-
打开文件
ofs.open(“文件路径”,打开方式);
-
写数据
ofs << “写入的数据”;
-
关闭文件
ofs.close();
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意: 文件打开方式可以配合使用,利用|操作符
**例如:**用二进制方式写文件 ios::binary | ios:: out
5.1.2读文件
读文件与写文件步骤相似,但是读取方式相对于比较多
读文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ifstream ifs;
-
打开文件并判断文件是否打开成功
ifs.open(“文件路径”,打开方式);
-
读数据
四种方式读取
//第一种方式
//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;
}ifs.close();
-
关闭文件
ifs.close();