C++核心编程
1 内存模型
C++程序在执行时,将内存大方向划分为4个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
1.1、程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
存放 CPU 执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
全局变量和静态变量存放在此,
全局区还包含了常量区,字符串常量和其他常量也存放在此
该区域的教据在程序结束后由操作系统释放
1.2、程序运行后
栈区:
由编译器自动分配释放,存放函数的参数值,局部变是等
注意事项:不要返回同部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new
在堆区开辟内存
2 引用
2.1、引用基本操作
int a=10;
int &b=a;
注意
- 引用必须初始化
int &b; // 错误的
- 引用一旦初始化,就不可以更改
2.2、引用做函数参数
引用传递:使用形参修改实参,简化了指针修改实参
void test(int &a, int &b)
2.3、引用做函数返回值
int& test(int a, int b)
- 不要返回局部变量的引用
int& test(int a, int b){
int a=10;
return a;
} // 错误的,函数结束后内存被释放
- 函数的调用可以作为左值
int& test(int a, int b){
static int a=10;
return a;
} // 静态变量,存放在全局区,在系统结束后才会被释放
函数作左值: test()=1000
;
2.4、引用的本质
引用本事是一个指针常量(指针的指向不可修改,指针的值可以修改)int * const
2.5、常量引用
主要用来修饰形参,防止误操作
只能读不能写
void show(const int &a){
cout<< a <<endl;
}
3 函数提高
3.1、函数默认参数
int func(int a,int b,int c=10){}
注意
- 某个位置已经有了默认参数,则之后的参数都要有默认参数
- 如果函数声明有默认参数,则函数实现就不能有默认参数
3.2、函数占位参数
int func(int a,int ){}
注意
- 使用时占位参数必须有实参
- 占位参数可以有默认参数
int func(int a,int =10 )
- 占位参数如何使用?
3.3、函数重载
函数名相同,参数类型、个数、顺序不同
函数返回值不能作为重载条件
注意
- 引用作为重载条件
int func(int &a){}
int func(const int &a){}
int c=10
func(c); //调用第一个 int &a=c;
func(c); //调用第二个 const &a=10;
- 重载碰到函数默认参数
int func(int a){}
int func(int a, int b=10){}
func(a) // 有二义性,错误
4 类和对象
封装、继承、多态
4.1、封装
4.1.1 封装的意义
class 类名 { 访问权限: 属性/行为 }
4.1.2 访问权限
- public 公共权限 成员类内可以访问,类外也可以访问
- protected 保护权限 成员类内可以访问,类外不可以访问 子类可以访问父类的保护内容
- private 私有权限 成员类内可以访问,类外不可以访问 子类不可以访问父类的私有内容
struct和class的区别
默认权限不同,struct:公有,class:私有
4.1.3 成员属性设置为私有
- 可以控制读写权限
- 对于写操作,可以检测数据有效性
4.2、对象初始化和清理
4.2.1 构造函数和析构函数
构造函数:创建对象时赋值
析构函数:销毁前自动调用
构造函数: 类名(){}
- 可以有参数,可以重载
- 没有返回值,只会调用一次
析构函数:~类名(){}
- 没有返回值,只会调用一次
- 不能有参数,不可重载
4.2.2 构造函数的分类
两种分类方式:
按参数:有参构造、无参构造(默认构造)
按类型:普通构造、拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
- 无参构造:
Person(){}
- 有参构造:
Person( int a ){}
- 普通构造:
Person(){}
- 拷贝构造:
Person( const Person &p ){}
- 括号法:
Person p1/ Person p2(10)/ Person p3(p2)
调用默认构造方法不需要加(),因为编译器会认为是一个函数声明 - 显示法:
Person p1/ Person p2=Person(10)/ Person p3=Person(p2)
不要用拷贝构造初始化一个匿名对象,编译器会认为你在创建一个重名对象 - 隐式转换法:
Person p1=10; //相当于 Person p1=Person(10)
Person p1=p2; //相当于 Person p1=Person(p2)
4.2.3 拷贝构造函数的调用时机
class Person{
public:
int m_Age;
public:
Person(){
}
Person(int age){
m_Age=age;
}
Person(const Person &p){
m_Age=p.m_Age;
}
}
- 使用一个已经创建完毕的对象来初始化一个新对象
Person p1(10);
Person p2(p1);
- 值传递的方式给函数参数传递
void work( Person p){}
Person p;
work(p); //值传递会创建一个临时副本
- 以值方式返回局部对象
void work(){
Person p1;
return p1;
}
Person p=work(); //局部对象返回时,会创建一个新的对象调用拷贝构造函数
4.2.4 构造函数的调用规则
默认情况,c++编译器会添加三个函数:默认构造函数、默认析构函数、默认拷贝构造函数
调用规则:如果用户提供了构造函数,c++就不用默认的了
4.2.5 深拷贝与浅拷贝
浅拷贝:简单赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
class Person{
public:
int m_Age;
int *m_Height;
public:
Person(){
}
Person(int age, int height){
m_Age=age;
m_Heigh=new int(height);
}
// 自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person &p){
m_Age=p.m_Age;
m_Height=new int(*p.m_Height);
}
~Person(){
}
}
4.2.6 初始化列表
用来初始化属性 构造函数(): 属性1(值1), 属性2(值2), 属性3(值3)
Person(int a, int b):m_Age(a),m_Height(b){}
4.2.7 类对象作为类成员
先构造A对象,再构造B对象
先析构B对象,再析构A对象
class A{}
class B{
A a;
}
4.2.8 静态成员
- 静态成员变量(有访问权限)
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化 - 静态成员函数(有访问权限)
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变是才属于类的对象上
class Person{ //空对象占1个字节内存,C++编译器给每个空对象一个字节用于区分
int m A;//非静态成员变量 属于类的对象上
static int m_B;//静态成员变量 不属于类对象上
void func(){} //非静态成员函数 不属于类对象上
static void func(){} //非静态成员函数 不属于类对象上
}
4.3.2 this指针
this指针指向被调用的成员函数所属的对象
this指针的本质是指针常量,指针的指向不能修改
this的用途:
区分同名的形参与成员变量
在非静态成员函数中如果要返回对象本身,return *this
4.3.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
4.3.4 const修饰成员函数
常函数:
- 成员函数后加
const
我们称为这个函数为常函数 - 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable
后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
class Person{
public:
//常函数
void showPerson() const{
}
int a;
mutable b;
}
const Person p; //常对象
4.4 友元
目的是让一个函数或类可以访问到一个类的私有成员
友元的关键字为friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
Bclass Building{
friend void goodGay(Building *building); //全局函数做友元
friend class GoodGay; //类做友元
friend void GooGay::visit(); //成员函数做友元
public:
Building(){
m_SittingRoom = "客厅"
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;//客厅
private:
string m_BedRoom;// 卧室
}
void goodGay(Building *building){
cout<< building->m_SittingRoom << building->m_BedRoom << endl;
}
4.5 运算符重载
4.5.1 加号运算符重载
实现两个自定义数据类型相加
//通过成员函数重载+号 (根据不同参数,operator+也可以重载)
Person operator+(Person &p){
Person temp;
temp.m_A = this->n_A + p.m_ A;
temp.m_B = this-m B+P.m B;
return temp;
}
Person p3=p1+p2;
对于内置的数据类型的表达式的的运算符是不可能改变的、不要滥用运算符重载
4.5.2 左移运算符重载
可以输出自定义的数据类型
class Person{
public:
//利用成员函数重载 左移运算符p.operator<<(cout) 简化版本加<<cout
//不会利用成员函数重载<<运算符,因为无法实现 cout在左侧
void operator<<( cout ){}
int m A;
int m B;
}
//只能利用全局函数重载左移运算符
ostream& operator<<( ostream &cout, Person &p ){} //本质 operator<<(cout,p),简化 cout<<p
cout<<p<< "" <<endl;
4.5.3 递增运算符重载
class MyInt{
public:
MyInt(){
m_Num=0;
}
//重载前置++运算符(返回引用)
MyInt& operator++(){
m_Num++;
return *this;
}
//重载后置++运算符(返回值,不返回引用)
MyInt operator++(int){ //int代表占位参数,用于区分前置和后置递增
MyInt temp=*this; //先记录
m_Num++; //再增加
return temp; //返回记录值
}
private:
int m_Num;
}
4.5.4 赋值运算符重载
c++编译器至少给一个类添加4个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=,对属性进行值拷贝
class Person{
public:
int *m_Height;
public:
Person(int height){
m_Heigh=new int(height);
}
Person& operator=(Person &p){
//编译器提供浅拷贝
//m_Height=p.m_Height;
//应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝if(m _Age != NULL)
delete m Age;m Age = NULL:
//深拷贝
m Age = new int(*p.m Age)
return *this;
}
~Person(){
if(m_Heigh!=NULL){
delete m_Height;
m_Heigh=NULL;
}
}
}
4.5.5 关系运算符重载
class Person{
public:
int m_Height;
public:
Person(int height){
m_Heigh=new int(height);
}
//关系运算符重载
bool& operator>(Person &p){}
}
4.5.6 函数调用运算符重载
函数调用运算符()也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数
仿函数没有固定写法,非常灵活
class Person{
public:
int m_Height;
public:
//关系运算符重载
bool& operator()(int a){}
}
Person p;
p(10);
4.6 继承
4.6.1 继承基本语法
语法:class 子类(派生类) : 继承方式 父类(基类)
class BasePage{
public:
void head(){};
void foot(){};
}
class Java : public BasePage{
public:
void content(){};
}
4.6.2 继承方式
继承方式一共有三种:
- 公共继承:父类中:子类中 public:public;protected:protected;private:不可访问;
- 保护继承:父类中:子类中 public:protected;protected:protected;private:不可访问;
- 私有继承:父类中:子类中 public:private;protected:private;private:不可访问;
4.6.3 继承中的对象模型
父类中所有的非静态成员都会被子类继承下去
私有成员只是被隐藏了,还是会继承下来
class Base{
public:
int m A;
protected:
int m B;
private:
int m_C; //只是被隐藏了,还是会继承下来
}
//利用开发人员命令提示工具查看对象模型
//cd 具体路径
//查看.cpp文件命名
cl /dl reportSingleClassLayout类名 文件名
4.6.4 继承中的构造和析构
子类继承父类后,构造与析构的顺序:
- 构造函数:先父类,后子类
- 析构函数:先子类,后父类
4.6.4 继承中的同名成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
如果想访问父类中的同名成员函数,需要加作用域
class Base{
public:
int m_A;
void func(){}
}
class Son:public Base{
public:
int m_A;
void func(){};
}
Son s;
s.m_A; //子类中的m_A
s.Base::m_A; //父类中的m_A
s.func; //子类中的func
s.Base::func; //父类中的func
4.6.5 继承中的静态成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
4.6.6 多继承语法
语法:class 子类 : 继承方式 父类1, 继承方式 父类2
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
4.6.6 菱形继承
菱形继承概念:
两个派生类继承同一个基类
又有某个类同时继承者两个派生类
这种继承被称为菱形继承,或者钻石继承
菱形继承问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性
- 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以
利用==虚继承(virtual)==可以解决菱形继承的问题
class Animal{
public:
int m_Age;
}
//加virtual后变为虚继承
class Sheep : virtual public Animal{}
class Tuo: virtual public Animal{}
class YangTuo : public Sheep,public Tuo
4.7 多态
4.7.1 多态的基本概念
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别: - 静态多态的函数 地址早绑定-编译阶段确定函数地址
- 动态多态的函数 地址晚绑定-运行阶段确定函数地址
class Animal(
public:
//虚函数
virtual void speak(){
cout<< "动物在说话"<<endl;
}
)
class Cat : public Animal{
virtual void speak(){
cout<< "猫在说话"<<endl;
}
}
//若地址早绑定,则执行Animal.speak()
//地址晚绑定,执行 Cat.speak()
void doSpeak(Animal &animal){ //Animal *animal = cat
animal.speak();
}
Cat cat;
doSpeak(cat);
动态多态满足条件: 1、有继承关系;2、子类重写父类的虚函数;
动态多态的使用:父类的指针或引用 指向子类对象
4.7.2 多态的底层原理
4.7.3 纯虚函数和抽象类
多态中,父类的虚函数的实现毫无意义,主要调用子类重写的方法,因此可以将虚函数改为纯虚函数
语法:virtual 函数返回类型 函数名 (参数列表)=0
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
4.7.4 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
可以解决父类指针释放子类对象
都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
class Animal(
public:
virtual void speak(){
cout<< "动物在说话"<<endl;
}
//虚析构:可以解决父类指针释放子类对象不干净的问题
virtual ~Animal(){}
//纯虚析构
virtual ~Animal()=0;
)
//纯虚析构需要有具体实现
Animal::~Animal(){
cout<< "纯虚析构" <endl;
}
class Cat : public Animal{
public:
Cat(string name){
m_Name=new string(name);
}
virtual void speak(){
cout<< "猫在说话"<<endl;
}
string *m_Name;
~Cat(){
if(m_Name!=NULL){
delete m_Name;
m_Name=NULL;
}
}
}
Animal *animal=new Cat("Tom");
delete animal; //父类的指针在析构的时候不会调用子类的析构函数。如果子类有存在堆区的数据,会造成内存泄漏
5 文件操作
包含头文件<fstream>
文件类型分为两种:
- 文本文件-文件以文本的ASCII码形式存储在计算机中
- 二进制文件下文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- 1.ofstream:写操作
- 2.ifstream:读操作
- 3.fstream:读写操作
5.1 文本文件
5.1.1 写文件
写文件步骤如下:
1.包含头文件
#include <fstream>
2.创建流对象
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:.in
5.1.2 读文件
读文件步骤如下:
1.包含头文件
#include <fstream>
2.创建流对象
ifstream ifs,
3.打开文件并判断文件是否打开成功
ifs.open("文件路径",打开方式);
4. 读数据
四种方式读取
//第一种
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 x;
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是读写的字节数
Person{
char name[64];
int age;
}
Person p = {"张三",18};
ofs.write((const char*)&p,sizeof(Person));
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len)
;
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
Person{
char name[64];
int age;
}
if(ifs.is_open()){
Person p;
ifs.read((char*)&p,sizeof(Person));
}