探秘C++之回炉重造

备注:本笔记作查漏补缺用,只记重点,只记干货,多的一句也不啰嗦!

一、C++基础知识查漏补缺

1.1 C++数据类型

1.1.1 整型数据

数据类型占用空间取值范围
short(短整型)2字节-2^15 ~ 2^15 - 1
int(整型)4字节-2^31 ~ 2^31 - 1
long(长整型)Windows为4字节,Linux为4字节(32位OS)或8字节(64位OS)-2^31 ~ 2^31 - 1
long long(长长整型)8字节-2^63 ~ 2^63 - 1

备注:使用sizeof()可以获取某一变量或数据类型所占的字节数

1.1.2浮点型数据

数据类型占用空间有效数字范围
float4字节7位有效数字
double8字节15~16位有效数字

备注

  1. 默认情况下输出一个浮点数最多显示到小数点后5位
  2. 可以使用科学计数法表示一个浮点数,如0.003可表示为3e-3

1.1.3 字符型数据

C/C++中字符型变量只占用1个字节

字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码放入存储单元(在0~256范围内int和char可以相互转换

1.1.4 字符串型数据

  • C风格字符串

    char 变量名[] = "字符串内容";

  • C++风格字符串

    string 变量名 = "字符串内容";

1.1.5 布尔类型

bool类型数据只有两个值:

  • true —— 真(本质是1)
  • false —— 假(本质是0)

bool类型占1个字节大小

1.2 C++函数

1.2.1 函数的声明与定义

  • 函数的声明方式

    type 函数名 (参数列表);
    
  • 函数的定义方式

    type 函数名 (参数列表){
        …
        函数体
        …    
        return 返回值;
    }
    

备注:函数可被多次声明,但只能被定义一次

1.2.2 函数的分文件编写

作用:让代码结构更清晰

函数分文件编写一般有4个步骤:

  1. 创建后缀名为 .h 的头文件
  2. 创建后缀名为 .cpp 的源文件
  3. 在头文件中进行函数的声明
  4. 在源文件中进行函数的定义

1.3 C++指针

1.3.1 定义和使用指针

//指针定义的语法:数据类型 *指针变量名;
int a = 10;
int *p = &a;
cout << "*p = " << *p << '\n' << "p = " << p << endl;
//=> *p = 10
//   p = 00AFF77C

备注:指针使用时前面不加 * 代表的是一个内存地址,加上 * 则代表的是这个内存地址中存储的数据

1.3.2 指针变量所占的内存空间

指针变量在32位操作系统中所占的内存空间为4个字节,在64位操作系统中占8个字节。

1.3.3 const修饰指针

  • const修饰指针——常量指针

    int a = 20;
    int b = 10;
    const int *p = &a;
    *p = 10;//错误,常量指针指向可以改,但指向的值不能改
    p = b;//正确
    
  • const修饰变量——指针常量

    int a = 20;
    int b = 10;
    int * const p = &a;
    *p = 10;//正确,指针常量指向不能改,但指向的值可以改
    p = b;//错误
    
  • const既修饰指针又修饰变量

    int a = 20;
    int b = 10;
    const int * const p = &a;
    *p = 10;//错误,const既修饰指针又修饰变量时指向不能改,指向的值也不能改
    p = b;//错误
    

二、C++核心编程

2.1 内存分区模型——内存四区

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理

    存放CPU执行的机器指令

    代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

    代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

  • 全局区:存放全局变量和静态变量以及常量

    全局变量和静态变量存在于此

    全局区还包含了常量区,字符串常量和其他常量也存放在此

    该区的数据在程序结束后由操作系统释放

  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等

    由编译器自动分配释放,存放函数的参数值,局部变量等

    注意事项:不要返回局部变量的地址,栈区开辟的数据在局部代码块执行完后由编译器自动释放。第一次使用局部变量地址时编译器会保留局部变量数值,第二次就被抹去了。

  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统进行回收

    由程序员分配释放,若程序员不释放,程序结束时由操作系统释放

    在C++中主要使用 new 关键字在堆区开辟内存,利用 new 创建的数据,会返回该数据对应类型的指针

    注意事项:堆区开辟的数据,由程序员手动开辟手动释放,释放时使用关键字 delete

内存四区的意义:

不同区域存放的数据,赋予不同的生命周期,使编程更灵活

备注:代码区、全局区在程序执行前构建,栈区和堆区在程序执行后构建

2.2 C++面向对象

C++面向对象的三大特性为:封装、继承和多态

C++认为万事万物都是对象,对象上由其属性和行为

2.2.1 构造函数

  • 分类

    • 按参数分:有参构造和无参构造
    • 按类型分:普通构造和拷贝构造
  • 调用方式

    class Person{
    private:
        int age;
    public:
        Person(){}//无参构造
        Person(int a){//有参构造
            age = a;
        }
        Person(const Person &p){//拷贝构造
            age = p.age;
        }
    };
    
  1. 括号法

    void test01(){
      Person p;//无参构造函数的括号法调用
      Person p2(10);//有参构造函数的括号法调用
      Person p3(p2);//拷贝构造函数的括号法调用,将p2身上的属性拷贝给p3
    }
    

    注意:调用无参构造函数时切记不要写括号,否则编译器会认为那是一个函数声明,而不是在创建对象

  2. 显示法

    void test01(){
    	Person p;//无参构造函数的显示法调用
    	Person p2 = Person(10);//有参构造函数的显示法调用
    	Person p3 = Person(p2);//拷贝构造函数的显示法调用
    }
    

    注意

    • 此方法可用于创建匿名对象,匿名对象特点:当前行执行结束后,系统会立即回收掉匿名对象
    • 不要使用拷贝构造函数初始化匿名对象,编译器会报重定义错误,因为编译器会认为Person(p3) 等价于 Person p3
  3. 隐式转换法

    void test01(){
        Person p2 = 10;//等价于Person p2 = Person(10);
        Person p3 = p2;//等价于Person p3 = Person(p2);
    }
    
  • 拷贝构造函数的调用时机

    C++中拷贝构造函数调用的时机通常有三种情况:

    • 使用一个已经创建完毕的对象来初始化一个新对象
    • 值传递的方式给参数传值
    • 以值方式返回局部对象
  • C++中构造函数的生成规则

    默认情况下,C++编译器至少给一个类生成3个函数

    1. 默认构造函数(无参,函数体为空)
    2. 默认析构函数(无参,函数体为空)
    3. 默认拷贝构造函数(对所有属性进行值拷贝)

    生成规则:

    • 如果用户定义有参构造函数,编译器不再生成默认无参构造,但会生成默认的拷贝构造
    • 如果用户定义拷贝构造函数,那么编译器不会再生成其他构造函数
  • 深拷贝与浅拷贝

    浅拷贝:简单的赋值操作

    深拷贝:在堆区重新申请空间,进行拷贝操作

    注意:所有由编译器自动生成的拷贝构造函数,都是浅拷贝;想要深拷贝就必须自己写能实现深拷贝的拷贝构造函数,尤其是牵扯到指针变量的拷贝,都属于深拷贝

  • 构造函数的偷懒写法——初始化列表

    //Person类的构造函数
    Person(int age, double height){
        this->age = age;
        this->height = height;
    }
    
    //初始化列表写法
    Person(int age, double height): this->age(age), this->height(height)
    {}//注意:函数体虽为空,但不能省略
    

    注意:初始化列表方式支持使用隐式转换法构造对象成员

2.2.2 友元

在程序里,有些私有属性也想让类外的一些特殊函数或者类访问,就需要用到友元技术。友元的目的就是让一个函数或者类访问另一个类中私有成员,关键字为friend

  • 全局函数做友元

    class House{
        //在类中声明友元函数
        friend void goodFriend(House &house);
    public:
        Building(){
            this->livingRoom = "客厅";
            this->bedRoom = "卧室";
        }
    private:
        string livingRoom;
        string bedRoom;
    }
    
    void goodFriend(House &house){
        //在House类的友元函数中查看House对象的所有成员
        cout << "全局函数goodFriend正在访问 " << house.livingRoom;
        cout << "全局函数goodFriend正在访问 " << house.bedRoom;
    }
    
    
  • 友元类

    class House; //House类的声明,避免编译器报错
    
    class GoodFriend{
        //定义goodFriend友元类
    public:
        House *house;
        GoodFriend();
        void visit();//参观函数
    };
    GoodFriend::GoodFriend(){
        house = new House;
    }
    void GoodFriend::visit(){//访问House对象中的成员
        cout << "GoodFriend类正在访问 " << house->livingRoom << endl;
        cout << "GoodFriend类正在访问 " << house->bedRoom << endl;
    }
    
    class House(){
        friend class GoodFriend;//声明GoodFriend类是House类的友元类
    public:
        string livingRoom;
        House();
    private:
        string bedRoom;
    };
    House::House(){
        this->livingRoom = "客厅";
        this->bedRoom = "卧室";
    }
    
  • 成员函数做友元

    class House;
    class GoodFriend{
    public:
        House *house;
        GoodFriend(){
            house = new House;
        }
        void visit(){//友元函数
            cout << "visit函数正在访问 " << house->livingRoom << endl;
            cout << "visit函数正在访问 " << house->bedRoom << endl;
        }
        void nvisit(){//与友元成员函数区别的普通成员函数
            cout << "nvisit函数正在访问 " << house->livingRoom << endl;
            cout << "nvisit函数正在访问 " << house->bedRoom << endl;//报错,不是友元,无法访问House的私有成员
        }
    };
    
    class House{
        friend void GoodFriend::visit();//声明友元成员函数
    public:
        string livingRoom;
        House(){
            livingRoom = "客厅";
            bedRoom = "卧室";
        }
    private:
        string bedRoom;
    };
    

2.2.3 运算符重载

运算符重载就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

  • Complex复数类,支持加减运算

    #include<iostream>
    using namespace std;
    
    class Complex
    {
    private:
    	double real;//实部
    	double virt;//虚部
    public:
    	Complex();//无参构造函数
    	Complex(double real, double virt);//含参构造函数
    	Complex operator+(Complex &other);//加号重载
    	Complex operator-(Complex& other);//减号重载
    	void print();//输出函数
    };
    
    //类中方法的实现
    Complex::Complex(){
    	real = 0;
    	virt = 0;
    }
    Complex::Complex(double real, double virt) {
    	this->real = real;
    	this->virt = virt;
    }
    Complex Complex::operator+(Complex& other) {//复数加法的重载
    	Complex temp(this->real + other.real, this->virt + other.virt);
    	return temp;
    }
    Complex Complex::operator-(Complex& other) {//复数减法的重载
    	Complex temp(this->real - other.real, this->virt - other.virt);
    	return temp;
    }
    void Complex::print()
    {
    	if (virt >= 0)
    	{
    		cout << real << " + " << virt << "i" << endl;
    	}
    	else
    	{
    		cout << real << " - " << -virt << "i" << endl;
    	}
    }
    
    

    注意

    1. Java中的toString方法在C++中用重载左移运算符 << 来实现,且只能用全局函数重载左移运算符:(以复数类为例)

      //传ostream&是为了返回cout对象,返回cout对象是为了实现链式输出
      ostream& operator<<(ostream &cout, Complex &c){
          if (virt >= 0)
      	{
      		cout << c.real << "+" << c.virt << "i" << endl;
      	}
      	else
      	{
      		cout << c.real << "-" << -c.virt << "i" << endl;
      	}
          return cout;
      }
      
      //为了使全局函数能够访问到Complex类的私有成员,我们可以将这个全局函数设为Complex类的友元
      class Complex{
          friend ostream& operator<<(ostream &cout, Complex &c);
          ……
      };
      
    2. 递增运算符的重载

      class MyInteger {
      	//声明友元函数
      	friend ostream& operator<<(ostream& cout, MyInteger m);
      private:
      	int data;
      public:
      	MyInteger(int d) {
      		data = d;
      	}
      	MyInteger& operator++() {//前置++运算符重载
      		data++;
      		return *this;//返回自身是为了能够实现连续操作
      	}
          MyInteger operator++(int) {//后置++运算符重载
      		//因为后置++运算的运算逻辑是先用后加,所以
      		//1. 记录当时的结果
      		MyInteger temp = *this;
      		//2. 递增
      		this->data++;
      		//3. 返回递增前的结果
      		return temp;
      		//不返回自身的引用是为了防止当前对象被编译器当作局部变量在函数执行完后直接释放
      	}
      };
      
      ostream& operator<<(ostream& cout, MyInteger m) {//<<运算符号重载
      	cout << m.data;
      	return cout;
      }
      
    3. 赋值运算符重载

      编译器默认的赋值运算符是浅拷贝,类的属性中有用到深拷贝时就需要对赋值运算符进行重载了

      class Person {
      	friend ostream& operator<<(ostream& cout, Person& p);
      private:
      	int* age;//创建一个堆区的属性
      public:
      	Person(int age) {
      		this->age = new int(age);
      	}
      	~Person() {
      		if (age != nullptr)
      		{
      			delete(age);//释放堆区所占内存
      			age = NULL;//防止非法访问,需要彻底断绝age与内存的联系
      		}
      	}
      	Person& operator=(Person& p) {
      		//先判断自身是否已经赋初值
      		if (age == nullptr) {
      			delete age;//若已经赋初值则释放掉
      		}
      		age = new int(*p.age);
      		return *this;//返回自身是为了实现连续赋值
      	}
      };
      ostream& operator<<(ostream& cout, Person& p) {//重载<<运算符
      	cout << *p.age << endl;
      	return cout;
      }
      
    4. 关系运算符重载

      //例:实现Complex复数类中 == 和 != 号的重载
      bool operator==(Complex& other);//判断两个复数是否相等
      bool operator!=(Complex& other);//判断两个复数是否不相等
      
      //具体实现
      bool Complex::operator==(Complex& other)//判断两个复数是否相等
      {
      	if (real == other.real && virt == other.virt) {
      		return true;
      	}
      	else {
      		return false;
      	}
      }
      
      bool Complex::operator!=(Complex& other)//判断两个复数是否不相等
      {
      	if (real == other.real && virt == other.virt) {
      		return false;
      	}
      	else {
      		return true;
      	}
      }
      
    5. 仿函数——重载函数调用运算符()

      仿函数在STL部分用处很大,因为该运算符实现后与真正的函数调用别无二致,因此得名仿函数。仿函数没有固定的写法,非常灵活

      class MyPrint {//实例:打印输出类
      public:
      	void operator()(string text) {//重载函数调用运算符
      		cout << text << endl;
      	}
      };
      
      void test() {
      	MyPrint m;
      	m("hello, world!");//函数调用运算符的用法
      }
      

2.2.4 多态

基本概念

多态是C++面向对象三大特性之一

  • 多态分为两类:

    1. 静态多态:函数重载 和 运算符重载 属于静态多态,复用了函数名
    2. 动态多态:派生类虚函数 实现运行时多态
  • 静态多态和动态多态的区别:

    1. 静态多态的函数地址早绑定 —— 编译阶段 确定函数地址
    2. 动态多态的函数地址晚绑定 —— 运行阶段 确定函数地址
  • 多态的实现

    实现静态多态只需要重载函数或运算符即可,使其在代码编译时就能准确找到需要调用哪些方法或者运算符;而实现动态多态则需要使用到 virtual 关键字来构建虚函数和抽象类,通过在子类中重写对应的函数来实现动态多态

  • 多态的优点

    1. 代码组织结构清晰,方便定位bug
    2. 可读性强
    3. 利于前期和后期的扩展和维护

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数

语法:virtual 返回值类型 函数名(参数列表) = 0;

当类中有了纯虚函数,这个类也就称为抽象类

抽象类特点:

  1. 无法实例化对象
  2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到了堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构纯虚析构

虚析构和析构的共性

  • 可以实现父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构的区别

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构的语法:virtual ~类名(){}

纯虚析构的语法:

class 类名{
    virtual ~类名() = 0;
};
类名::~类名(){
    析构代码
}

注意:纯虚析构需要声明也要实现,所推荐使用虚析构

三、C++进阶部分

3.1 C++文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束就会被释放

通过文件可以将数据持久化

C++中对文件操作需要包含头文件**<fstream>**

3.1.1 文件类型及对应操作分类

文件类型分为两种:

  1. 文本文件 —— 文件以文本的ASCII码形式存储在计算机中
  2. 二进制文件 —— 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream:读操作
  3. fstream:读写操作

3.1.2 文本文件的读写操作

  • 写文件操作

    写文件操作的步骤:

    1. 首先要包含头文件

      #include <fstream>
      
    2. 创建流对象

      ofstream ofs;
      
    3. 使用流对象打开文件

      ofs.open("文件路径", 打开方式);
      

      文件的打开方式:

      打开方式解释
      ios::in为读文件而打开文件
      ios::out为写文件而打开文件
      ios::ate初始位置:文件尾部
      ios::app追加方式写文件
      ios::trunc如果文件存在,先删除,再创建
      ios::binary二进制方式写文件

      注意:文件打开方式可以配合使用,需要借助 | 逻辑或运算符 ,例如以二进制方式写文件:ios::binary | ios::out

    4. 写数据

      ofs << "写入的数据";
      
    5. 关闭文件

      ofs.close();
      

    总结

    • 文件操作必须包含头文件fstream
    • 读文件可以用ofstream,或者fstream类
    • 打开文件时候需要指定操作文件的路径,以及打开方式
    • 利用<<运算符可以向文件中写数据
    • 操作完毕要记得关闭文件
  • 读文件操作

    读文件与写文件步骤类似,但是读取方式相对较多

    读文件步骤如下:

    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;
      	}
      
      	//第四种读方式
      	char c;
      	while ((c = ifs.get()) != EOF) {//EOF = End Of File
      		cout << c;
      	}
      
    5. 关闭文件

      ifs.close();
      

    总结

    • 读文件可以利用ifstream,或者fstream类
    • 利用is_open()函数判断文件是否打开成功
    • close()函数关闭文件

3.1.3 二进制文件的读写操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值