C++核心编程--引用、函数高级、类和对象、文件操作
1、内存分区模型
c++程序执行时,将内存大方向分为四个区域:
代码区:存放函数体的二进制代码,由操作系统进行管理的
全局区:存放全局变量和静态变量以及常量
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
堆区:由程序员分配和释放,若程序员不释放,程序结束时有操作系统回收
内存四个区域意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程
程序运行前:
在程序编译后,生成了exe代码可执行程序,未执行该程序前分为两个区域
1.1、程序运行前:全局区、代码区
代码区:
存放cpu执行执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
全局变量和静态变量存放在此
全局区还包括常量区,字符串常量和其他变量也存放在此
该区域的数据在程序结束后由操作系统释放
全局区:全局变量、静态变量、static关键字;常量:字符串常量、const修饰的全局变量(全局常量)。
不在全局区:局部变量,const修饰的局部变量(局部常量)。
总结:1、在程序运行之前分为全局区和代码区
2、代码区特点是共享和只读
3、全局区存放全局变量、静态变量、常量
4、常量区存放const修饰的全局常量和字符串常量
1.2、程序运行后:栈区、堆区
栈区:由编程器自动分配释放、存在函数的参数值,局部变量等;
注意事项:不要返回局部变量地址,栈区开辟的数据由编译器自动释放
第一次可以打印正确的数字,是因为编译器做了保留,第二次这个数据就不保留了。
堆区:由程序员分配和释放,若程序员不释放,程序结束时有操作系统回收。在c++中,利用new关键字开辟内存。
1.3、new操作符
C++中利用new操作符在堆区开辟数据,堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
语法:new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
int a = new int(10); 10 代表新建数据10 释放数据 delete a;
new创建数组:
int arr = new int[10]; 10代表十个元素 释放数据 delete[] arr;
2、引用
2.1、引用基本语法
引用:给变量起别名;
语法:数据类型 &别名 = 原名
2.2、引用注意事项
1、引用必须初始化;int &b;错误
2、引用一旦初始化后,就不可以更改
2.3、引用做函数参数
作用:函数传值时,可以利用引用的技术让形参修饰实参;函数名(数据类型 &变量,数据类型 &变量)。
优点:可以简化指针修改实参
总结:通过引用参数产生的效果通按地址传递是一样的,引用的语法更简单
2.4、引用做函数返回值
1、不要返回局部变量的引用
局部变量:存放栈区,函数运行完释放
2、函数的调用可以作为左值;返回静态变量引用。
左值:等号左边的数据
如果函数的返回值是引用,这个函数调用可以作为左值存在;
静态变量:存放全局区,程序运行完系统释放;
2.4、引用的本质
本质:在C++内部实现就是一个指针常量;
int &ref = a; 相当于 int* const ret = &a;
指针常量是指针的指向不可改,也说明为什么引用不可更改。
ref = 10;
内部发现是ref是引用,自动帮我们转化为*ref = 20;
结论:c++推荐使用引用技术,因为语法方便,引用的本质是指针常量,但是所有的指针操作编译器帮我们做了。
2.5、常量引用
常量引用使用场景:用const来修饰形参,防止误操作
int & ref = 10;错误;引用必须是一块合法的内存空间。
const int & ref = 10;加上const之后,编译器将代码修改 int temp = 10;const int & ref = temp;
加上const之后只读,不可修改;
3、函数高级
3.1、函数的默认参数
语法:返回值类型 函数名 (参数 = 默认值){}
如果我们自己传入了数据,就用自己的数据,如果没有,那就用默认值
注意事项:
1、 如果某个位置已经有了默认值,那么这个位置往后,从左到右都必须有默认值。
2、如果函数的声明有默认参数,函数的实现就不能有默认参数。声明和实现只能有一个默认参数。
3.2、函数占位参数
c++中函数的形参列表里可以有占位参数,用来占位,调用函数时必须填补位置
语法:返回值类型 函数名(数据类型){}
3.3、函数重载
作用:函数名可以相同,提高复用性
函数重载满足的条件:
1、同一个作用域
2、函数名相同
3、函数参数类型不同或者个数不同或者顺序不同
注意:函数的返回值不可以作为函数重载的条件
3.4、函数重载注意事项
1、引用作为重载条件
(int &a);
(const int &a);
传变量会进入(int &a),因为变量可读可写,const修饰的只读不可写;直接传数字可以进入(const int &a);
2、函数重载碰到默认参数
当函数碰到默认参数,会产生二义性,报错,尽量避免这种情况
4、类和对象
c++面向对象的三大特性:封装、继承、多态
4.1、封装
封装的意义:将属性和行为作为一个整体,表现生活中的事物;将属性和行为加以权限控制。
封装意义一:在设计类的时候,属性和行为写在一起,表现事物
语法:class 类名{ 访问权限:属性 / 行为};
访问权限:
公共权限 public 成员 类内可以访问 类外可以访问
保护权限 protected 成员 类内可以访问 类外不可以访问 儿子可以访问父亲的保护内容
私有权限 private 成员 类内可以访问 类外不可以访问 儿子不可以访问父亲的私有内容
class和struct区别:
struct 默认权限是 公有 public 因此可以访问
class 默认权限是 私有private 类外不能访问
成员设置私有优点:
1、将所有成员属性设置为私有,可以自己控制读写权限
2、对于写权限,我们可以检测数据的有效性
4.2、对象的初始化和清理
1、构造函数和析构函数
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
构造函数语法:类名 (){}
1、构造函数没有返回值也不写void
2、函数名字和类名相同
3、构造函数可以有参数,因此可以发生重载
4、程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
按照参数分:有参构造、无参构造
按照类型分:普通构造、拷贝构造
拷贝构造函数:类名(const 类名 &对象){}
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
析构函数语法:~类名 (){}
1、析构函数没有返回值也不写void
2、函数名字和类名相同,在名称前加上~
3、构造函数不可以有参数,因此不可以发生重载
4、程序在对象销毁前会自动调用构造,无需手动调用,而
2、调用构造的三种方法:
1、括号法
类名 P1; 默认构造函数
类名 p2(10);有参构造
类名 p3(p2);拷贝构造
注意事项1:
调用默认构造函数时,不要加(),因为会认为是一个函数声明,不会创建对象。
2、显示法
类名 P1; 默认构造函数
类名 p2 = 类名(10);有参构造
类名 p3 = 类名(p2);拷贝构造
类名(10);匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
注意事项2
不要利用拷贝构造函数,初始化匿名对象 ,编辑器会认为类名(p3) ===类名 p3;会认为是对象声明
类名(p3);错误;
3、隐式转换法
类名 p4 = 10; 相当于 写了 类名 p4 = 类名(10):有参构造
类名 p5 = p4; 拷贝构造
3、拷贝构造函数调用时机
c++中拷贝构造函数调用时机通常有三种情况
1、使用一个已经创建完毕的对象来初始化一个新对象
2、值传递的方式给函数参数传值;当实参传递时,临时拷贝
3、以值方式返回局部对象;return的时候拷贝一个新的返回
4、构造函数调用规则
默认情况下,C++编译器至少给一个类添加三个函数:
1、默认构造函数(无参,函数体为空)
2、默认析构函数(无参,函数体为空)
3、默认拷贝构造函数,对属性值进行值拷贝
构造函数调用规则:
1、如果用户定义有参数,c++不提供默认无参构造,但是会提供默认拷贝构造
2、如果用户定义拷贝构造函数,c++不会提供其他构造函数
5、深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
如果利用编译器提供的拷贝构造函数,会做浅拷贝。浅拷贝的问题:堆区内存重复释放。
6、初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值1),……{}
7、类对象作为类成员
当其他类对象作为本类成员,构造时候先构造其他类对象,在构造自身,析构顺序与构造相反。
8、静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
1、静态成员变量
○ 所有对象共享同一份数据
○ 在编译阶段分配内存
○ 类内声明,类外 初始化
2、静态成员函数
○ 所有对象共享的一个函数
○ 静态成员只能访问静态成员变量
静态成员函数两种访问方式:1、通过对象访问 2、通过类名访问
静态成员函数也是有访问权限的:私有权限下类外也无法访问。
4.3、 C++对象模型和this指针
1、成员变量和成语函数分开储存
在C++中,类内成员变量和成员函数分开储存,只有非静态成员变量才属于类的对象上。
空对象占用内存空间为:1;c++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置。
非静态成员变量,属于类的对象上;静态成员函数,不属于类对象上;非静态成员函数,不属于类对象上。
2、this指针的用途
this指针指向被调用的成员函数所属的对象。
用途:1、当形参和成员变量同名时,可用this指针来区分
2、在类的非静态成员函数中返回对象本身,可使用return * this;
用return *this的时候,返回值类型是的引用 :类名 &函数名{return *this};返回值的形式 类名函数名,是临时拷贝新对象;返回引用不会创建新的对象。
3、空指针调用成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针;如果用到this指针,需要加以判断保证代码的健壮性
4、const修饰成员函数
常函数:
○ 成员函数后加const后我们会称之为常函数
○ 常函数不可以修改成员属性
○ 成员属性声明时加关键字mutable后,在常函数中依然可以修改,常对象下也可以修改
常对象:
○ 声明对象前加const称之为常对象
○ 常对象只能调用常函数
4.4、 友元
友元的目的就是让一个函数或者类访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现:
○ 全局函数做友元 friend 返回值 函数名();
○ 类做右元 friend class 类名;
○ 成员函数做友元 friend class 类名;
4.5、 运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
1、加号运算符重载
作用:实现两个自定义数据类型相加的运算
○ 成员函数重载+加号
Person operator+(Person &p){
Person temp; temp.A = this.A + p.A;
temp.B = this.B + p.B;
}
调用:Person p3 = p1.operator+(p2);
简化为:Person p3 = p1 + p2;
○ 通过全局函数重载+
Person operator+(Person &p1,Person &p2){
Person temp; temp.A = p1.A + p2.A;
temp.B = p1.B + p2.B;
}
调用:Person p3 =operator+(p1,p2);
简化为:Person p3 = p1 + p2;
总结:对于内置的数据类型的表达式的运算符是不可能改变的;不要滥用运算符重载。
2、左移运算符重载
作用:可以输出自定义的数据类型
左移运算符只能用全局函数来重载左移运算符
void operator<<( ostream &cout ,Person &p){
cout <<"m_A = "<<p.m_A<<"m_B = "<<p.m_B;
}不能链式输出<<endl;
返回自身引用,就可以链式输出<<endl;
ostream & operator<<( ostream &cout ,Person &p){
cout <<"m_A = "<<p.m_A<<"m_B = "<<p.m_B;
return cout;
}
3、递增运算符重载
作用:通过重载递增运算符,实现自己的整型数据
前置递增:返回引用是为了一直对一个数据进行递增操作
MyInteger & operator++(){
m_Num++;
return *this;
}先计算后返回
后置递增:返回值,因为临时局部变量函数运行完释放。
MyInteger operator++(int){
MyInteger temp = m_Num;
m_Num++;
return temp;
} 先记录后计算最后返回记录的值
4、递增运算符重载
C++ 编译器至少 给一个类添加四个函数:
1、默认构造函数(无参,函数体为空)
2、默认析构函数(无参,函数体为空)
3、默认拷贝构造函数,对属性值进行值拷贝
4、赋值运算符 operator=,对属性进行值拷贝
如果类中属性指向堆区,做赋值操作时也会出现深浅拷贝问题
例如: Person& operator=(Person &p){
if(m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
m_Age = new int (*p.m_Age); return *this;
}
5、关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
重载等于
bool operator==(Person &p){
if(this->m_Name ==p->m_Name&&
this->m_Age ==p->m_Age& ){
return ture;
}
return false;
}
重载不等于
bool operator==(Person &p){
if(this->m_Name ==p->m_Name&&
this->m_Age ==p->m_Age& ){
return false;;
}
return true;
}
6、函数调用运算符重载
○ 函数调用运算符()也可以重载
○ 由于重载后使用的方式非常像函数的调用,因此称之为仿函数
○ 仿函数没有固定写法,非常灵活
匿名对象:类名()创建匿名对象,运行完就释放
类名()();匿名对象调用仿函数
4.6、 继承
继承是面向对象三大特性之一
1、继承的基本语法
继承的好处:减少重复代码
语法:class 子类:继承方式 父类
子类 也称之为 派生类
父类 也称之为 基类
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
2、继承方式
父类私有的子类无法访问
公共继承:父类中的公共权限成员 到子类中依然是公共权限
父类中的保护权限成员 到子类中依然是保护权限
父类中的私有权限成员 子类中访问不到
保护继承:父类中的公共权限成员 到子类中变为保护权限
父类中的保护权限成员 到子类中依然是保护权限
父类中的私有权限成员 子类中访问不到
私有继承:父类中的公共权限成员 到子类中变为私有权限
父类中的保护权限成员 到子类中变为私有权限
父类中的私有权限成员 子类中访问不到
3、继承中的对象模型
父类中所有非静态成员属性都会被子类继承下去
父类中私有成员属性,是被编译器给隐藏了,因此是访问不到,但是确实被继承下去了
4、继承中的构造和析构顺序
子类继承父类后, 当创建子类对象,也会调用父类的构造函数
继承中的构造和析构的调用顺序如下:
先构造父类,在构造子类,析构的顺序与构造的顺序相反
5、继承同名成员的处理方式
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域;子类对象名.父类::成员名。
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数。
总结:1、子类可以直接访问到子类中的同名成员
2、子类对象加作用域可以访问到父类同名成员
3、当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
6、继承同名静态成员的处理方式
静态成员和非静态成员出现同名,处理方式一致
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域;子类对象名.父类::成员名。
同名静态成员的调用:
1、通过对象访问
子类静态成员调用:子类对象.静态成员
父类静态成员调用:子类对象.父类名::静态成员
2、通过类名访问
子类静态成员调用:子类名::静态成员
父类静态成员调用:子类名::父类名::静态成员
第一个::代表通过类名方式访问
第二个::代表访问父类作用域下
同名静态成员函数的调用:
1、通过对象访问
子类静态成员调用:子类对象.静态函数
父类静态成员调用:子类对象.父类名::静态函数
2、通过类名访问
子类静态成员调用:子类名::静态函数
父类静态成员调用:子类名::父类名::静态函数
第一个::代表通过类名方式访问
第二个::代表访问父类作用域下
子类出现和父类同名的静态成员函数,也会隐藏父类所有的同名成员函数;如果想要访问父类中被隐藏同名函数,需要加作用域。
7、多继承语法
语法:class 子类:继承方式 父类1,继承方式 父类2,……
多继承可能会引发同名成员出现,需要加作用域来区分
通过Vs2017 开发者工具,进到保存cpp文件的位置,cl /d1 reportSingleClassLayout__类名___"文件名"。
通过这个方式查看类占了多少内存空间,分别都是什么。
8、菱形继承
菱形继承概念:
两个派生类继承同一个基类
又有某个类同事继承两个派生类
这种继承被称为菱形继承,或者钻石继承
菱形继承会产生二义性,利用虚继承来解决问题。
继承之前 加上关键字 virtual 变为虚继承
4.7、 多态
多态是面向对象三大特性之一
1、多态的基本概念
多态分为两类
○ 静态多态:函数重载和运算符重载属于静态多态,复用函数名
○ 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
○ 静态多态的函数地址早绑定 - 编译阶段确定函数地址
○ 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
函数前面加上virtual关键字,变成虚函数,那么编译器的时候就不能确定函数调用了
动态多态满足条件:
1、有继承关系
2、子类重写父类的虚函数
重写 :函数返回值类型、函数名、参数列表全部一致称为重写
动态多态使用:父类的指针或引用 执行子类对象
2、多态案例一——计算机类
多态的优点:代码组织结构清晰;可读性性强;利于前期和后期的扩展以及维护
3、纯虚函数和多态类
通常父类的虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类;
抽象类特点:
1.无实例化对象
2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类
4、多态案例二——制作饮品
案例描述:
制作饮品的大致流程:煮水- 冲泡 - 倒入杯中 - 加入佐料
利用多态技术实现本案例,提供抽象类制作饮品基类,提供子类制作咖啡和茶叶。
5、虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构共性:
1、可以解决父类指针释放子类对象
2、都需要有具体的函数实现
虚析构和纯虚析构区别:
1、如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名()= 0;纯虚析构声明
类名::~类名(){} 纯虚析构类外实现
纯虚析构 需要声明也需要实现
有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
总结:
1、虚析构和纯虚析构就是用来解决通过父类指针释放子类对象
2、如果子类中没有堆区数据,可以不写虚析构或纯虚析构
3、拥有纯虚析构函数的类也属于抽象类
4、多态案例三——电脑组装
案例描述:电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于储存);将每个零件封装出抽象类,并且提供不同的厂商生产不同的零件,例如Intel厂商和lenovo厂商;创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口,测试时组装三台不同的电脑进行工作。
5、文件操作
c++对文件操作需要包含头文件<fstream>
文本类型分为两种:
1、文本文件 文件以文本的ASCLL码形式存储在计算机中
2、二进制文件 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类:
1、ofstream:写操作
2、ifstream:读操作
3、fstream:读写操作
5.1、文本文件
写文件步骤:
(1)包函头文件:#include <fstream>
(2)创建流对象:ofstream ofs;
ofstream ofs("打开路径",打开方式);
(3)打开文件:ofs.open("打开路径",打开方式);
(4)写数据:ofs<<"写入的数据";
(5)关闭文件:ofs.close();
打开方式:ios::in 读方式
ios::out 写方式
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制
文件打开方式可以配合使用,利用|操作符
例如:用二进制方式写文件 ios::binary | ios::out
读文件步骤:
(1)包函头文件:#include <fstream>
(2)创建流对象:ifstream ifs; ifstream ifs("打开路径",打开方式);
(3)打开文件:ifs.open("打开路径",打开方式);
is_open函数判断文件是否打开成功
(4)读数据:四种方式读取
(5)关闭文件: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;
}
5.2、二进制文件
打开方式指定为 ios::binary
写文件:
二进制写文件主要利用流对象调用成员函数write
函数类型:ostream& write(const char * buffer,int len)
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
string 会容易报错 用char数组
二进制方式写文件:
(1)包函头文件:#include <fstream>
(2)创建流对象:ofstream ofs; ofstream ofs("打开路径",打开方式);
(3)打开文件:ofs.open("打开路径",打开方式);
(4)写文件:ofs.write((const char*)&p,sizeof(类名));
(5)关闭文件:ifs.close();
读文件:
二进制读文件主要利用流对象调用成员函数read
函数类型:istream& read(char * buffer,int len)
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数
二进制方式读文件:
(1)包函头文件:#include <fstream>
(2)创建流对象:ifstream ifs; ifstream ifs("打开路径",打开方式);
(3)打开文件:ifs.open("打开路径",打开方式);
is_open函数判断文件是否打开成功
(4)读文件:ifs.read((char *)&p,sizeof(类名));
(5)关闭文件:ifs.close();

浙公网安备 33010602011771号