初面网初面网

C++中级知识点

内存分区

内存分成四个区域:代码区、全局区、栈区、堆区。 程序运行前只有代码区、全局区

  1. 代码区:存放函数体的 二进制代码 ,由操作系统管理
  2. 全局区:存放 全局变量、静态变量以及常量
  3. 栈区:编译器自动分配释放,存放函数的参数值,局部变量等
  4. 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

不同区域存放不同数据,有利于方便管理生命周期,给予更多编程灵活性

new操作符

new操作符用于在堆区开辟数据。由程序员管理开辟,也由程序员管理释放。释放操作可以用delete。

利用new创建的数据会返回该数据对应类型的指针

int* func(){
    int* arr = new int[5];
    int* p = new int(10);
    // delete p;        该操作会释放p的地址空间,导致后续调用报错
    // delete[] arr;    释放数组时要加中括号!!!切记切记!!!
    return p;
}

引用

引用,用于给变量起别名,其本质是指针常量。

// 数据类型 &别名 = 原名
int a = 10;
int c = 20;
// 引用必须初始化,且不可再变更
int &ten = a;           // 实际上是赋值了地址

cout << ten << endl;   // 输出地址
cout << &ten << endl;   // 输出地址

ten = c;
cout << a << endl;      // 猜一下这三个输出什么
cout << b << endl;
cout << c << endl;

不要返回局部变量的引用

int& test01(){
    int a = 10;
    return a;
}

int main(){
    int& ref = test01();
    cout << ref << endl;    // 结果由系统做了保留,但实际上已经失去了对地址的控制权
    cout << ref << endl;    // 因为失去了控制权,地址中的值会被系统利用,所以值会变化

    // 省略system和return
}
int& test02(){
    static int a = 10;      // 静态变量,存放在全局区,在程序结束后由系统释放
    return a;
}

int main(){
    int& ref = test02();
    cout << ref << endl;
    cout << ref << endl;

    test02() = 521;

    cout << ref << endl;
    cout << ref << endl;

    // 省略system和return
}

常量引用

int& ref1 = 20;             // 不加const不允许引用常量。此句会报错。
const int& ref = 10;        // 加const之后,编译器会将代码视作:
// int temp = 10; const int& ref = temp;        也就是编译器会帮我们把常量创建成变量

函数进阶知识

默认参数

// 函数的形参列表中的形参是可以有默认值的
// 有传入用传入,无传入用默认
// 一个传入变量有默认,则后续传入变量都得有默认
// 声明里某个值提供了默认值,那么定义里不可以再给予默认值
// 返回值类型 函数名 (参数=默认值){}
int add(int a,int b=20,int c=30){
    return a+b+c;
}

int swap(int,int)   // 只提供类型起到占位作用。占位参数也可以有默认值
{

}

函数重载

函数名相同,提高复用性

需要满足的条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数 类型不同 或者 个数不同 或者 顺序不同

注意:函数的返回值不可以作为函数重载的条件

void func(){}

void func(int a){}

void func(int,float){}

// 引用作为重载条件
void func(int& a){}         // int a = 10;func(a);

void func(const int& a){}   // func(10);

// 函数重载遇到函数默认参数
void func(int b,int c = 10){}   
void func(int b){}                // 这两句在单参数调用时有二义性

面向对象

具有相同性质的对象,可以抽象为类。所以:

  • 对象,是具体的类,是类的实例化
  • 类,是诸多对象的抽象概念

面向对象有三大特征:封装、继承、多态

// 创建类
class Cube{             // 一个方盒类
public:
    // 属性,简单理解为变量
    int edge;
    // 行为,理解为函数
    int Volumn() {
        return edge*edge*edge;
    }
};                      // 不要忽略这个分号

int main(){
    Cube cube1;
    cube1.edge = 10;

    cout << "这是一个" << cube1.Volumn() << "立方米的立方体" << endl;

    // 省略system和return
}
/*
类里的权限有三种:public   protected    private
public: 成员类内可以访问,类外也可以访问    公共权限
protected:类内可以访问,类外不可访问       保护权限
private: 类内可以访问,类外不可访问         私有权限

保护权限与私有权限的区别:
在后续的  “继承”  篇章中,类可以写成父子关系。private中的内容,子类不可访问
*/

struct和class的区别

这两者都能够构建出一个类,但struct默认权限为公共,class默认权限为私有

构造函数、析构函数

这两个函数必须成对出现。它们的作用分别是:

  • 构造函数:在实例化对象时为成员属性赋值
  • 析构函数:对象销毁前,释放成员占用的空间

在没有手动编写它们时,它们由编译器提供,但没有代码块在函数体内(即只是一个空函数)

class Cube{
/*
    构造函数
    格式:类名(){}
    1、没有返回值也不写void
    2、函数名称与类名相同
    3、可以有参数,可以重载
    4、调用对象时系统自动调用
*/
    Cube(){

    }

    Cube(int length){
        return length*10;
    }
/*
    析构函数
    格式:~类名(){}
    1、没有返回值也不写void
    2、函数名称与类名相同,前面有~
    3、没有参数,不可以重载
    4、对象销毁前系统自动调用
*/
    ~Cube(){

    }

    // 拷贝构造 ( 其他非此种形式的叫做 普通构造 )
    Cube(const Cube &c){}
}

void test01(){ 
    Cube c1;
    Cube c2 = Cube(10);     // 有参构造
    Cube c3 = Cube(c2);     // 拷贝构造
    Cube(22);               // 匿名对象。特点:当前执行结束后,系统会立即回收匿名对象
    // Cube(c3);            // 不要利用拷贝函数初始化匿名对象  编译器会认为 Person(p3)===Person p3;即认为是对象声明
    Cube c4 = 33;           // 隐式转换法
    Cube c5 = c4;           // 隐式转换法的拷贝构造
}

void main(){
    // test01();
    Cube cube;          // 可以尝试注释这行,调用test01(),析构函数的出现是不同的
    //Cube cube();      // 此句会被认为是函数声明,而不是创建无参对象
    // 省略system和return
}
// 析构函数的执行时机是  对象销毁之前。当在main中创建类,其销毁时机是return前,所以由于我们有system中断了程序执行,此时对象并没有释放,所以析构函数暂时不会出现。按任意按键之后,才能够走到return,才能够释放对象,也就可以执行析构了

拷贝构造函数

调用时机:

  • 使用一个已经创建完毕的对象初始化一个新对象
  • 值传递的方式给函数参数传值
  • 值方式返回局部对象
class Person{
public:
    Person(){
        cout<<"Person默认构造函数调用" << endl;
    }

    Person(int age){
        m_Age = age;
        cout<<"Person有参构造函数调用" << endl;
    }

    Person(const Person &p){
        m_Age = p.m_Age;
        cout<<"Person拷贝构造函数调用" << endl;
    }

    ~Person(){
        cout<<"Person析构函数调用" << endl;
    }

    int m_Age;
}

void test01(){
    Person p1(20);
    Person p2(p1);
}

void test02(Person m){}
Person test03(){
    return Person p;
}

深拷贝、浅拷贝

  • 深拷贝:在堆区重新申请空间进行拷贝操作
  • 浅拷贝:简单的赋值拷贝操作

初始化列表

// 构造函数():属性1(值1),属性2(值2),...{}
class Cube(){
    Cube(): v_A(10),v_B(20),v_C(30){}   // 构造函数时将属性初始化
    // Cube(int a,int b,int c): v_A(a),v_B(b),v_C(c){}  // 使用a、b、c替代写死的数可以更灵活
    int v_A;
    int v_B;
    int v_C;
}

类对象作为类成员

class A{}
class B{
    A a;        // A是B的对象成员
}

静态成员

在成员变量、成员函数前加上static,则它们就变为静态成员变量,静态成员函数。

静态成员变量,所有对象共享同一份数据,编译阶段分配内存,类内声明,类外初始化

静态成员函数,所有对象共享同一个函数,静态成员函数只能访问静态成员变量

class Person{
public: 
  static int age;           // 类内声明
}

int Person::age = 25;       // 类外初始化

void test01(){
    Person p;
    cout << p.age << endl;
    cout << Person::age << endl;      // 由于静态成员不属于具体的对象,所以可以直接类名访问
}

this指针

每一个非静态成员函数只会诞生一份函数实例,即多个类型的对象会公用一块代码。那么如何区分哪个对象调用的呢?就需要使用this指针。

this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的本质是 指针常量,指针的指向不可更改

class Person{
public:
    Person(int age){
        this->age = age;
    }

    Person& addAge(Person &p){
        if(this==NULL){         // 为防止空指针异常,可以加上这个判断块
            return;
        }
        this->age += p.age;

        // this指向p2的指针,*this指向p2这个对象本体
        return *this;
    }

    int age;
};

void test01(){
    Person p1(18);      // this会指向p1.如果有其他对象,this也会分别指向那些对象
    Person p2(10);
    P2.addAge(p1).addAge(p1).addAge(p1);    // 链式调用
}

const修饰成员

class Person{
Public:
    // const Person * const this;
    void showPerson() const     // 常函数
    {
        this->m_A = 100;
    }

    int m_A;
    mutable int m_B;    // 特殊变量,即使在常函数中也可修改
};

void test01(){
    const Person p;     // 对象前加const变为常对象
    p.m_A = 100;        // 报错
    p.m_B = 100;        // 特殊值常对象下也可修改
    p.showPerson();     // 常对象只能调用常函数
}

友元

能够访问类里的私有成员的特殊 函数 ,叫做友元函数、友元类。

// 友元函数
class Building {
    friend void goodFriend(Building* building);     // 友元函数声明
public:
    Building() {
        m_age = 18;
    }
private:
    int m_age;
};

void goodFriend(Building* building) {
    cout << building->m_age << endl;
}

int main() {
    Building bd;
    goodFriend(&bd);

 system("pause");
    return 0;
}

// 友元类
class Building;
class GoodFriend {
public:
    GoodFriend();
    void visit();
    Building* building;
};

class Building {
    friend class GoodFriend;        // 友元类
private:
    int age;
};

GoodFriend::GoodFriend() {
    building = new Building();
}

void GoodFriend::visit() {
    building->age += 10;
    cout << building->age << endl;
}

void test01() {
    GoodFriend gf;
    gf.visit();
}

// 友元成员

class Building;
class GoodBoy {
public:
    GoodBoy();
    void visit();
    void visit2();
    Building* building;
};

class Building {
    friend void GoodBoy::visit();
private:
    int m_age;
};

Building::Building(){
    m_age = 10;
}

GoodBoy::GoodBoy(){
    building = new Building;
}

void GoodBoy::visit() { building->m_age; }
void GoodBoy::visit2() {}

void test01(){
    GoodBoy gb;
    gb.visit();
    gb.visit2();
}

运算符重载

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

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 = 10;
    Person p2;
    p2.m_a = 10;
    p2.m_b = 10;

    Person p3 = p1 + p2;
}

继承

减少重复代码,提高复用

// class 子类(派生类) : 继承方式 父类(基类)
class Animal{
public:
    void run();
}

class Cat:public Animal{}    // 公共继承,保护继承,私有继承

/*
前提:父类中的私有成员子类默认不可访问
公共继承,父类中成员权限在子类中保持原样
保护继承,父类中的公共权限、保护权限在子类中皆变为保护权限
私有继承,父类中的公共权限、保护权限在子类中皆变为私有权限
*/

继承中的对象模型

提问:子类最终继承了父类中的哪些成员?

答案

父类中所有非静态成员都会被子类继承

私有属性被编译器隐藏,但其实是会被继承的


继承中构造和析构顺序

提问:构造顺序是怎样的?析构顺序呢?

答案

构造时,先父后子

析构时,先子后父

class Base {
public:
    Base() { cout << "Base构造" << endl; }
    ~Base() { cout << "Base析构" << endl; }
};

class Son : public Base {
public:
    Son() { cout << "son构造函数" << endl; }

    ~Son() { cout << "son析构函数" << endl; }
};

继承同名成员

提问:子类与父类有同名成员,如何通过子类访问该成员的数据呢?

答案
  • 子类同名成员直接访问
  • 父类同名成员加作用域
class Base {
public:
    Base() {
        m_a = 100;
    }

    void SayNum() {
        cout << m_a << endl;
    }

    void SayNum(int inNum) {
        this->m_a = inNum;
        cout << m_a << endl;
    }
    int m_a;
};

class Son : public Base {
public:
    Son() {
        m_a = 200;
    }

    void SayNum() {
        cout << m_a << endl;
    }

    int m_a = 200;
};

void test01() {
    Son s;
    cout << s.m_a << endl;
    cout << s.Base::m_a << endl;
    s.SayNum();
    s.Base::SayNum();
    // s.SayNum(10);       // 会报错。
    s.Base::SayNum(10);
}

/*
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
如果想访问父类中被隐藏的同名成员函数,需要加作用域
*/

继承同名静态成员

提问:继承中,如何访问同名的静态成员? 答案:与非静态成员的处理方式一致

// 静态成员可以直接类名访问
Base::m_a;

// 这种形式意味着:用Son类名,访问Base作用域下的m_a,即前后双冒号含义是不同的
Son::Base::m_a;

多继承

// class 子类 : 继承方式 父类1, 继承方式 父类2, ... {}
class Son : public Base1, public Base2{}

菱形继承

两个派生继承同一基类,同时,有某个类继承了这两个派生类

缺点:这种继承方式,不仅存在二义性,还造成内存空间的资源浪费

解决:虚继承

class Animal{ public: int m_age; }

class Tiger : virtual public Animal{};  // 继承方式前加 virtual 可变成虚继承

class Lion : virtual public Animal{};

class TigerLion : public Tiger, public Lion{}

多态

多态分为两类:

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

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定(编译阶段确定函数地址)
  • 动态多态的函数地址晚绑定(运行阶段确定函数地址)

满足条件:

  1. 有继承关系
  2. 子类重写父类虚方法

动态多态使用: 父类的指针或者引用 指向子类对象

class Animal{
public:
    // 虚函数
    virtual void speak(){}
};

class Cat : public Animal{
public:
    void speak(){}
};

void doSpeak(Animal &animal){
    animal.speak();
}

void test01(){
    Cat cat;
    doSpeak(cat);
}

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现很少被调用,主要调用子类重写的内容,因此可以将虚函数改为 纯虚函数

如果一个类中有了纯虚函数,那么这个类就叫作 抽象类

抽象类:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
// virtual 返回值类型 函数名 (参数列表) = 0;
class Base{
public:
    virtual void func() = 0;
}

class Son : public Base{
public:
    virtual void func(){};
}

虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

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

区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象
class Animal{
public:
    virtual void speak() = 0;
};

class Cat:public Animal{
public:
    Cat(string name){
        m_Name = new string(name);
    }
    virtual void speak(){}

    string* m_Name;
};

void test01(){
    Animal* aimal = new Cat("Tom");
    animal->speak();
    delete animal;
}

C++文件操作

文件分成两种

  • 文本文件,以ASCII码形式存储在计算机中
  • 二进制文件,以二进制形式存储在计算机中

对文件操作,需要先包含头文件 <fstream>

/*
1. 包含头文件
#include <fstream>

2. 创建流对象
ofstream ofs;

3. 打开文件
ofs.open("文件路径", 打开方式);     // 可以用 | 符号写多个打开方式4

4. 写数据
ofs << "写入的数据";

5. 关闭文件
ofs.close();
*/
打开方式
打开方式 解释
ios::in 读文件
ios::out 写文件
ios::ate 初始位置:文件尾
ios::app 追加
ios::trunc 存在则先删除,再创建
ios::binary 二进制方式

文本文件——写入

ofstream ofs;
ofs.open("test.txt", ios::out);

ofs << "123123";

ofs.close();

文本文件——读取

ifstream ifs;
ifs.open("test.txt", ios::in);

if (!ifs.is_open()) {
    return;
}

// 第一种方法
/*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) {        // EOF = End of File
//    cout << c;
//}

ifs.close();

二进制——写入

class Person {
public:
    int age;
    void initAge();
};

void Person::initAge() {
    age = 10;
}

void test01() {
    ofstream ofs;
    ofs.open("testBinary.txt", ios::out|ios::binary);

    Person p;
    p.initAge();
    ofs.write((const char*)&p, sizeof(Person));

    ofs.close();
}

二进制——输出

ifstream ifs;
ifs.open("testBinary.txt", ios::in | ios::binary);

if (!ifs.is_open()) {
    return;
}

Person p;

ifs.read((char*)&p, sizeof(Person));
cout << p.age;
ifs.close();

更新于 2025/2/26