阅读前一定要注意:这篇笔记进行 C++ 期末复习时当作参考使用的,内容不全面也不够深入,如果要进行深入的学习也可以当作一点点参考,这篇笔记旨在让处于复习阶段的学生快速对期末考点进行复习和巩固。

第一章 C++ 入门

1、C++ 与 C语言

  1. C语言是面向过程的语言,但到后面面对日益复杂的业务问题,C语言渐渐不足以处理复杂的问题,于是在 C语言的基础上加了很多的新功能,成这个新版为 C++
  2. C++ 最本质的特征是面向对象
  3. C++ 是 C语言的一个超集:任何合法的 C程序都是合法的 C++程序
  4. C++不是一种纯正的面向对象的语言,而是一种多范式的编程语言。虽然C++支持面向对象编程,它也支持其他编程范式,包括过程式编程和泛型编程。

2、Hello World

1)示例代码

#include<iostream>
/*
我是一个
多行注释
*/
int main() {
	// 我是一个单行注释
	std::cout << "Hello World!" << std::endl;
	return 0;
}
  • 其中 std 是名字空间,关于名字空间的具体介绍将放在后面。

2)C++ 的编译过程

C++ 是 C语言的一个超集:任何合法的 C程序都是合法的 C++程序,由这句话不难看出,C++ 的编译过程和 C语言的编译过程是相同的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、C++ 的输入与输出功能

利用标准输入流 cin 和标准输出流 cout 来实现输入和输出

#include<iostream>
using namespace std;
int main() {
	int a;
	cin >> a;
	cout << a << endl;
	return 0; 
}
  • 利用 iostream 库中的,std 名字空间下的,cin 和 cout 对象

  • 用法:
    std :: cin >> 存放输入的地方

    std :: cout << 输出的数据

  • 利用重载的左移和右移运算符作为流提取运算符

4、名字空间

​ 在库文件中定义命名空间的目的是为了确保库的组件(函数、类、变量等)与用户代码之间不发生命名冲突。这是因为库文件通常是独立开发的,可能包含了大量的代码和命名,如果没有命名空间,库的组件可能会与用户代码中的其他组件发生命名冲突,导致编译错误或意外行为。

在库文件中使用名字空间的作用:

  1. 避免命名冲突:库文件可以将其所有组件放入一个命名空间中,这样用户在使用库时,可以通过命名空间限定符访问库的组件,从而避免与用户代码中的其他同名组件发生冲突。
  2. 提供清晰的组织结构:命名空间提供了一种在库中组织代码的方式,使库的组件彼此相关联,并有助于用户理解库的结构和用法。
  3. 隔离实现细节:库的内部实现细节可以放在库的命名空间中,而用户只需关心公共接口。这有助于隐藏库的实现细节,提高了封装性。
  4. 支持库版本控制:库的开发者可以更轻松地管理库的版本,因为库的不同版本可以使用不同的命名空间,从而避免不同版本之间的冲突。

我们只需要关注第一条就可以,考试中一般不会出现更难的运用

5、引入的新的类型

1)string 类

string 是 C++ 中的字符串类型,它的本质其实就是一串字符。

​ C++ 中新增了 string 类来表示字符串,并且提供了一系列的方法来供我们使用,string 实际上是对 char[] 的封装

#include<iostream>
using namespace std;
int main() {
	string s = "ABC";
	cout << s.length() << endl;
	cout << s[1];
}

2)bool 类型

​ C++ 引入了 bool 类型,它的取值只能是 true 或者 false

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 它的本质就是单字节无符号的 0 或 1。

第二章 函数

1、函数的基本概念

1)函数的概念

  • 函数是面向过程的体现,实现了数据的输入、处理和返回的步骤。
  • 做到了将重复发生的过程进行统一的描述,实现了代码的复用和解耦。

2)函数的定义和声明

函数的声明明确了函数:

  1. 接收什么参数
  2. 返回什么值

具体进行了什么操作则可以放在函数的实现中完成

#include<iostream>
using namespace std;
int add(int a, int b);
int main() {
	int a = 0, b = 1;
	cout << add(a, b);
}
int add(int a, int b) {
	return a + b;
}

3)函数的实参和形参

  • 形式参数:书写函数的时候用来描述函数体的变量
  • 实际参数:调用函数的时候外部实际传输给函数的变量
  • 即如果不使用指针传参或者引用传参的话,对形式参数的任何操作都是对实际参数没有影响的。

4)函数的值传参和指针传参

#include<iostream>
using namespace std;
void swap(int* a, int* b);
int main() {
	int a = 0, b = 1;
	swap(&a, &b);
	cout << a  << " " << b << endl;
}
void swap(int* a, int* b) {
	int temp;
	temp = *a;
	*a = *b;
	*b = temp;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 我们一定要意识到函数的值传参和指针传参本质上是没有区别的,只不过一个传递的是地址,另一个传递的是值而已

2、函数参数的默认值

  • 默认值:告诉程序当数据或者指令缺失的时候,默认应该用什么去代替
#include<iostream>
using namespace std;
class A {
public:
	A() {};
	int n = 10;
};
int main() {
	A a1;
	cout << a1.n;
}

​ 注意:当我们没有显式的定义默认构造函数的时候是无法使用 A a; 这样的方式去构建一个新的对象的,会提示未初始化。

3、引用与引用传参

​ 前面我们已经演示了指针传参来交换两个变量的值,但里面要写到大量的 * 来指定指针所指向的值,这时候就可以用到引用传参来解决这种问题。

引用:

​ 引用实质上就是同一个实体(变量)的别名,可以把它当作这个对象来进行任何操作,引用底层实际上是使用了指针的,也因为它的别名的性质,所以不存在空引用****,即一开始使用引用的时候就必须指定引用的对象。

案例:

​ 这时,我们就可以通过引用传参来改写上面的函数

#include<iostream>
using namespace std;
void swap(int& a, int& b);
int main() {
	int a = 0, b = 1;
	swap(a, b);
	cout << a << " " << b << endl;
}
void swap(int& a, int& b) {
	int temp;
	temp = a;
	a = b;
	b = temp;
}

4、函数的重载

​ 当我们需要用相同的操作处理多组类型、数量不同的数据的时候,就需要写多个函数,每个函数名不同,有多少个参数列表就需要写多少个函数名,给我们编写程序带来了很大的不便。

这时候就可以允许多个同名的函数存在,每个函数去处理不同的类型或者数量的参数,这就是函数重载

#include<iostream>
using namespace std;
int add(int a, int b);
int add(int a, int b, int c) {
	return a + b + c;
};
int main() {

	cout << add(1, 2) << " " << add(1, 2, 3) << endl;
}
int  add(int a, int b) {
	return a + b;
}

需要注意的是函数的重载允许返回类型的不同,但能仅有返回类型不同,函数的重载主要还是针对参数列表的。

#include<iostream>
using namespace std;
int add(int a, int b);
int add(int a, int b, int c) {
	return a + b + c;
}
float add(float a, float b) {
	return a + b;
}
int main() {

	cout << add(1, 2) << " " << add(1, 2, 3) << " " << add(1.1f, 2.2f) << endl;
}
int  add(int a, int b) {
	return a + b;
}

函数重载存在的问题
  • 虽然函数重载解决了代码调用的代码冗余,但还是存在相同的问题,但我们必须保证当调用的时候,程序必须知道我们调用的是哪一个函数。
  • 而匹配是由编译器完成的,当存在多个匹配的重载函数的时候就会产生歧义。
int f(int a, int b);
int f(int& a, int& b);
int f(int a, int b, int c = 10);

​ 这时候再去调用的时候编译器就无法知道我们调用的是哪个函数。

总结
  • 函数的重载和返回值无关,只需要满足函数的名字相同但是参数列表不同即可
  • 并且在调用的时候不能产生歧义

5、内联函数

​ 当一个简单的函数被反复调用的时候,就会导致调用所占的比重逐渐增大,这时候就需要考虑如何减少这部分的比重。

​ 这时候我们就可以让编译器在编译的时候直接将函数在调用的地方展开,避免函数的重复调用引发的开销,而这就是函数内联

内联函数:
  1. 建议编译器在编译的时候将某个函数在调用的地方直接展开,避免运行时的开销
  2. 需要注意的是,并不是只要写了内联函数就一定会被内联,内联是发生在编译的时候,由编译器是否采用。
#include<iostream>
using namespace std;
inline int add(int a, int b);
int main() {
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	cout << add(1, 2);
}

第三章 抽象与封装

1、面向对象思想

  • 随着计算机的发展,问题的场景变得越来越复杂,规模越来越大,面向过程的编程思想在编写大型程序的时候已经很难使用。
  • 系统随着实体类型的增多,变得越来越复杂,这时候再去描写它的过程就会变得极其费力,这时候如果我们转而去描写这些实体类型,那么只要正确的描述每一个实体类型就可以完成整个系统的运行。
  • 这时就不难引出描述实体的两个方面:实体的属性和实体的行为(方法)

比如举一个例子,
如果我们想要计算三角形、长方形和正方形其中之一的面积,这时候按照面向过程的方法就需要编写一个过程,实现输入和判断是哪种类型,再根据这个类型去完成面积的计算。
如果是面向对象的话,我们就会考虑设置三个对象来分别描述这三个形状,里面定义它的长度属性和计算面积的方法,然后通过调用对象中的方法来计算,这时候我们主方法中的代码就会减少很多。

2、类和对象

  • 定义一个类:就需要定义它的属性和它的行为,我们常常称之为成员变量和成员函数。
  • 一定不要忘记写分号
class A {
// 访问修饰控制符
public:
    // 成员变量
	int x;
	int y;
    // 成员函数
	int add() {
        return x + y;
    }
private:
    int z;
}

1)结构体和类

通过上面的学习,我们不难发现类和之前的结构体似乎差不多,实际上
类 = 结构体 + 成员函数,事实上 C++ 也支持再结构体中定义少量的方法。

  • 我们通常根据使用的场景来选择使用结构体还是使用类
    • 结构体:主要用来记录数据,行为极少
    • 类:既有属性也有行为

结构体的使用:

#include <iostream>
#include <string>

// 定义一个结构体
struct Person {
    std::string name;
    int age;
    double height;
};

int main() {
    // 创建结构体变量并初始化
    Person person1;
    person1.name = "Alice";
    person1.age = 25;
    person1.height = 1.75;

    // 访问结构体成员并输出信息
    std::cout << "Name: " << person1.name << std::endl;
    std::cout << "Age: " << person1.age << std::endl;
    std::cout << "Height: " << person1.height << " meters" << std::endl;

    return 0;
}

2)构造函数和析构函数

​ 构造函数和析构函数是两种特殊的成员函数。

  • 构造函数:对象实例化的时候,再分配的空间上构造对象如初始化成员变量,分配资源等等
  • 析构函数:对象生命周期结束的时候,回收空间前完成对象的清理工作
  • 构造函数和析构函数都没有返回值
#include<iostream>
using namespace std;
class A {
	int* ptr;
public:
	A(){
		ptr = (int*)malloc(sizeof(int));
		cout << "构造函数\n";
	}
	~A() {
		cout << "析构函数";
	}
};
int main() {
	A a;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3)this 指针

比如我们在使用构造函数的时候,为了区别赋值的形参和成员参数,必须要给他们起不同的名字,导致了含义的不清晰,那有什么方法可以解决当前的问题吗?

class A {
public:
	int length;
	A(int lengthA) {
		n = x;
	}
};

​ this 再类定义的内部进行使用,用于指向当前对象,这时候我们就可以这样写

class A {
public:
	int length;
	A(int length) {
		this->length = length;
	}
};

​ 表示当前使用这个构造函数的对象

3、封装

封装:
将类的成员变量或者成员函数设置为私有的,不允许外界的直接访问,但可以通过我们提供的方法来实现对这些参数的操作。

为了实现封装,C++ 提供了三种访问修饰符来指定成员变量或者成员函数的使用范围

访问修饰符 允许访问范围
public 允许类内部、类的对象、外部代码以及子类访问
protected 允许类内部和子类访问
private 只允许类内部访问

如果我们不指定访问修饰符的话,默认的会使用 private 修饰符。

1)Getter / Setter

  • 为某些私有的成员变量提供的外部读写的方法
class A {
private:
	int a;
public:
	A(){}
	~A(){}
	int getA()const{
		return a;
	}
	void setA(int a) {
		this->a = a;
	}
};

​ 可以在 get 方法后面添加 const 修饰来指定这个成员函数不能对成员变量进行修改操作,相当于一种规范。

第四章 继承和多态

1、继承与派生

​ 当我们编写一个学生类的时候,他会有年龄、身高、体重等属性,编写一个老师类的时候也会包含这些属性,还会有相同的成员方法,这时候我们就可以从中抽象出一个 Person 类,老师和学生都是一个 Person,这时候他们只需要继承 Person 中共同的成员变量和成员方法即可,无需再自己编写。

​ 这时候我们就可以将学生获得 Person 成员的这一过程称为继承,而 Person 向学生传递这些成员的操作就称为派生

#include <iostream>

class Base {
protected:
    int baseVar;

public:
    Base(int value) : baseVar(value) {}

    void displayBaseVar() {
        std::cout << "Base's baseVar: " << baseVar << std::endl;
    }
};

class Derived : public Base {
private:
    int derivedVar;

public:
    Derived(int baseValue, int derivedValue) : Base(baseValue), derivedVar(derivedValue) {
        // 子类构造函数使用不同的值初始化父类和子类的成员变量
    }

    void accessBaseAndDerivedVars() {
        displayBaseVar(); // 通过子类对象访问父类的成员变量
        std::cout << "Derived's derivedVar: " << derivedVar << std::endl; // 访问子类的成员变量
    }
};

int main() {
    Derived derivedObj(42, 10); // 使用子类构造函数分别初始化父类和子类的成员变量
    derivedObj.accessBaseAndDerivedVars(); // 通过子类对象访问父类和子类的成员变量

    return 0;
}

2、虚函数

根据上面的继承我们可以知道,一个对象可以有多重身份,且这些身份是层级递进的关系。

但实际操作的时候会产生如下的问题:

  • 父类中某些行为需要在子类中进行具象化
  • 父类中的某些行为是不能确定的,必须在子类中进行实现

​ 由此就产生了虚函数的概念,

  • 父类的虚函数可以在子类中进行重写,即重新实现,但参数和返回类型必须保持一致,方便实现向下转型。
  • 注意:当父类的函数没有表明是虚函数的时候,子类如果和父类重名就仅仅是普通的重名,并不属于重写。
  • 含有虚函数的类称为虚类

3、纯虚函数和抽象类

要理解为什么要有纯虚函数我们可以尝试去理解接口:
还是按照上面的计算面积的例子来举例,我们每个图形对象都实现了计算面积的方法,为了对这些对象进行统一的管理,我们就可以通过一个接口来规范他们的这些行为,这些接口就是我们定义的规范,继承它的子类只需要去实现它即可,我们只需要对接口进行操作即可。

  • 某些函数是抽象的而非具体的,不可以独立存在,所以不能被实例化

  • 纯虚函数:在基类中不实现,仅声明为纯虚函数,留在子类中去实现。

    class Person {
    public:
    	int age;
    	string name;
    	Person(){}
    	Person(int age) {
    		this->age = age;
    	}
        // 纯虚函数
    	virtual void say() = 0;	
    };
    
  • 含有纯虚函数的类叫做抽象类,仅含有纯虚函数的类叫做接口

  • 抽象类和接口都不能实例化

  • 需要注意的是,父类中的纯虚函数如果不在子类中实现的话,子类就会被视作抽象类,无法被实例化

4、多态

1)多态的具体体现

  • 方法的多态:重写和重载就体现了方法的多态
  • 对象的多态(重点)
    • 一个对象的运行类型和编译类型可以不一样
    • 编译类型在定义对象的时候,就确定了,不能改变
    • 运行类型是可以变化的,比如可以指向父类也可以指向子类

2)C++ 中多态的实现

​ C++ 中依靠虚函数的重写实现方法的多态,依靠指向子类对象的父类指针来实现对象的多态

#include<iostream>
#include<string>
using namespace std;
class Person {
public:
	int age;
	string name;
	Person(){}
	Person(int age) {
		this->age = age;
		cout << "父类的构造函数被调用,年龄为" << age << endl;
	}
	void say() {
		cout << "我是一个人类";
	}
};
class Student : public Person{
public:
	Student(int age): Person(1){
		this->age = age + 100;
	}
	void say(){
		age = 10;
		cout << "我是一个学生";
	}
};

int main() {
	Person* p = new Student(10);
	(*p).say();
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 这里实际上的通过虚函数表来确认执行的是子类的方法,简单来说,就是这个指针会先去调用父类的方法,但如果父类方法被重写的话,这个方法就会指向子类中已经重写的方法,那这个指针最终实际调用的就是子类的方法。

3)总结

  • 通过虚函数和指向子类对象的父类指针,可以把不同的子类统一视为其共同的父类。
  • 于是无需针对不同的子类写相同的代码,将其统一视作父类,比如在构建对象数组的时候,只需要一个父类的数组就能容纳所有的子类对象。
  • 本质是将虚函数的能做什么和怎么做分离出来,基类指定要做什么,子类来实现具体的做法。
  • 如果父类中的函数没有声明为虚函数,也就是子类并未重写这个函数那么将调用父类三种的方法。

第五章 知识点拓展

1、深拷贝与浅拷贝

​ 当我们对一个对象进行拷贝的时候,拿指针举例,当一个对象拷贝另一个对象的时候,如果我们仅仅是拿一个指针指向另一个对象指针所指向的空间而没有生成属于自己的空间的话,我们就将这样的拷贝称为浅拷贝,反之则称为深拷贝。

1)拷贝构造函数

  • 拷贝已经存在的同类对象来构建新的对象,所以参数是对象的引用

  • A(A& other) {
    	this->n = other.n;
    	//浅拷贝
    	this->ptr = other.ptr;
    }
    

    这时默认的拷贝构造函数,采用的是浅拷贝的方式,如果类内有间接资源,要记得自己声明一个深拷贝构造函数

  • 拷贝函数默认是进行浅拷贝,如果我们需要进行深拷贝的话则需要重写拷贝构造函数

    A(A& other) {
    	this->n = other.n;
    	//浅拷贝
    	this->ptr = (char*)malloc(100);
    	memcpy(this->ptr, other.ptr, 100);
    }
    

2、运算符重载

​ 运算符可以视作函数,比如 cout << x; 实际上就是调用了 cout 对象的成员函数 << 而 x 就是这个函数的参数

​ 下面我们来看一个具体的例子,有一个坐标系,我们要实现坐标系下两个向量的加法,如果仅仅采用函数的形式来实现的话会导致可读性较差而且不美观,下面来看看运算符重载是如何实现的:

#include<iostream>
#include<string>
using namespace std;
class Vector {
public:
	int x;
	int y;
	Vector(int x, int y) : x(x), y(y) {};

	Vector operator+(Vector v1) {
		int add_x = this->x + v1.x;
		int add_y = this->y + v1.y;
		return Vector(add_x, add_y);
	}
};

int main() {
	Vector v1(1, 2);
	Vector v2(2, 3);
	Vector v3 = v1 + v2;
	cout << "(" << v3.x << ", " << v3.y << ")" << endl;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1)重载运算符 =

​ 下面来介绍一个较为特殊的重载运算符,= :

  • 如果在构造一个对象的时候使用等号和另一个已经构造好的对象进行连接,则默认调用的是拷贝构造函数

  • 但如果是两个已经构造好的对象用等号连接,则使用的是重载的等号运算符

  • 等号运算符默认情况下进行的是浅拷贝,如果有间接对象则需要构造深拷贝的等号运算符,如下方的重载运算符可以实现连续赋值

    	Vector& operator=(Vector& v1) {
    		this->x = v1.x;
    		this->y = v1.y;
    		return *this;
    	}
    

3、类的静态成员

​ 类的静态成员指的是用 static 修饰的成员变量或者成员函数,生命周期伴随整个程序,不随着对象的销毁而销毁。

1)静态成员变量的使用

  • 静态成员在类内是可以直接访问的,但是在类外需要使用 类名 :: 变量名 去访问静态成员也可以通过实例化的对象去访问
  • 静态成员必须在类外进行单独的初始化,格式为 数据类型 类名 :: 变量名 = 初始值。

2)类的静态函数的使用

  • 类的静态函数必须在类的声明时声明,在类的定义时定义
  • 类的静态函数由于其特殊的性质,不能调用类中的非静态成员变量
  • 需要注意的是类的静态成员函数和类的友元函数不属于类的成员函数
  • 类的静态函数可以通过类的实例化对象和 类名 :: 函数名 来访问。

4、继承下的构造函数和析构函数的顺序

当存在继承的时候构造函数的调用是由父类到子类,而析构函数是相反的顺序

如 A 是 B 的父类而 B 是 C 的父类,那么当构建一个 C 的实例的时候,实际构造函数的调用顺序是 A、B、C,析构函数则相反

5、构造函数的参数列表

先来看一个例子,B 类中有一个成员变量 A,但是 A 没有默认的构造函数,

 class B {
 	A a;
 	int n;
 	B(int n) {
 		this->n = n;
 		a = A(n);
 	}
 };

这样还能实现初始化吗?

经过验证是不能的,因为这样的话默认会先构造 a,再去执行赋值操作,这时发现没有默认的构造函数,就会报错
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这时候就需要寻找一个在构造函数之前执行的构造 a 的方法,就是下面要说的参数列表

我们可以通过以下的方式来对 A 进行赋值

 class B {
 	A a;
 	int n;
 	B(int n) : A(n), n(n) {}
 };

这样就不会出现上述的情况

6、额外补充

下面的一些知识点考试频率较小,只需要记忆即可,不必去深入理解

1)多继承和菱形继承

多继承
  • C++ 允许多继承的存在,即一个子类继承多个不同的父类

    class B : public A, private C, protected D {};
    
  • 即使用指定的继承方式来继承父类中的成员

菱形继承
  • 那这样就不可避免的出现菱形继承的问题,即子类继承的两个父类存在相同的父类,这样我们在子类中去使用父类继承来的成员的时候就不可避免的出现二义性,即编译器不知道我们调用的是哪个父类的成员变量

  • 这时候就可以同构虚继承的方式来解决这个问题

    #include <iostream>
    
    class Animal {
    public:
        void eat() {
            std::cout << "Animal is eating" << std::endl;
        }
    };
    
    class Mammal : public virtual Animal {
    public:
        void run() {
            std::cout << "Mammal is running" << std::endl;
        }
    };
    
    class Bird : public virtual Animal {
    public:
        void fly() {
            std::cout << "Bird is flying" << std::endl;
        }
    };
    
    class Bat : public Mammal, public Bird {
    public:
        void navigate() {
            std::cout << "Bat is navigating" << std::endl;
        }
    };
    
    int main() {
        Bat bat;
        bat.eat();  // 调用从 Animal 继承的成员函数
        bat.run();  // 调用从 Mammal 继承的成员函数
        bat.fly();  // 调用从 Bird 继承的成员函数
        bat.navigate();  // 调用 Bat 类自身的成员函数
        return 0;
    }
    
  • 将父类的继承声明为虚继承,那么子类无论如何使用继承来的成员函数或者成员变量,都仅仅会生成一个父类的副本。

2)友元函数

通过友元函数,外部的类可以访问到类中的 private 和 protected 访问修饰符修饰的成员

#include<iostream>
#include<string>
using namespace std;
class A {
private:
	int a = 10;
public:
	friend void Asay(A& temp);
};
class B {
public:
	A a;
	void Bsay() {
		Asay(a);
	}
	

};
void Asay(A& temp) {
    friend void Asay(A& temp);
}
int main() {
	B b;
	b.Bsay();
	A a;
	Asay(a);
}

友元函数不属于类的成员函数,它的定义是可以在类外的而且不需要加限定,如上述例子。

3)虚函数表

  • 虚函数表就是 C++ 底层实现多条的原理
  • 每个对象都有一个虚函数表,记录了虚函数的指针,当子类重写了父类的虚函数的时候,就会重写父类虚函数指针指向的函数
  • 这样就导致通过指针来调用会指向子类重写的新的函数

第六章 类的动态内存分配

1、程序内存模型

下面我们来看一个经典的 C++ 内存模型:

​

栈:变量、对象和函数等需要的空间由编译器管理,我们无需关心内存的申请和释放,编译器会自动进行维护

堆:开放给程序员自己来管理,需要自己来进行申请使用和释放,编译器不会进行维护

2、面向对象内存分配

构造一个对象需要如下的步骤

  1. 申请需要大小的内存
  2. 构造对象
  3. 返回引用指针
  4. 通过指针来使用构造好的对象
  5. 析构对象
  6. 释放对象的空间

前三步的指针通过 new 运算符来获取,最后两步通过 delete 运算符来获取。

注意当我们进行了例如下面的操作时,必须在析构函数将指针释放,否则会影响程序的运行

int* ptr = (int*)malloc(sizeof(int)*3);
A* a = new A();
// 自动调用析构函数
delete a;

除了上述的方法还能通过 new[] 来获取含有多个对象的数组,这样获得的内存空间只能通过 delete[] 来进行释放,且只能用于 new[] 出来的指针

class A{};
int main() {
	A* a = new A(6);
	delete[] a;
}

3、虚析构函数

1)内存溢出和内存泄漏

内存溢出:内存的空间不够用了

内存泄漏:已经使用的内存空间没有及时的释放,

内存泄漏会增大内存溢出的概率

2)虚析构函数

我们假设一个场景,如果一个类 B 继承了一个类 A,且类都含有一个指向内存空间的指针,这时候我们用 A 的指针指向构造了的 B 的一个对象
A* a = new B(); 那 A 中的构造函数也会被构造,分配到指针的内存空间,这时候我们仅 delete a; 那么仅仅会调用 A 中的析构函数,不会调用子类的析构函数。

解决方法就是将父类的析构函数设置为虚函数,这样就会先调用子类重写的析构函数,然后根据析构顺序又回去调用父类的析构函数,就完成了两个对象的析构

第七章 模板

1、模板和泛型思想

​ 先来设想一个场景,如果我们要实现多个数字的相加,但他们的类型不同,应该如何实现呢?

首先想到的肯定是使用函数的重载,但是这样会造成代码的实现方的冗余,一样的逻辑要重写三遍

那是不是可以先不确定参数的类型,等到参数实际传入的时候再去确定类型呢?这样就引入了今天的模板

#include<iostream>
#include<string>
using namespace std;
template<typename T1, typename T2>
string Add(T1 a, T2 b) {
	
	return to_string(a + b);
}
int main() {
	cout << Add<int, int>(1, 2) << endl;
	cout << Add<float, int>(1.1, 3);
}

2、模板函数

​ 实际上在上面的例子中就已经使用了模板函数,通过 template 指定下面使用的是一个模板, typename 表示下面定义的是一个类型名,T1 和 T2 是类型的占位符,他们的具体属性将会在编译的时候由编译器来确认。

需要注意的是如果程序通过上下文可以得知需要赋予这两个占位符的类型就可以不用加尖括号,如上面的程序可以改写为:

#include<iostream>
#include<string>
using namespace std;
template<typename T1, typename T2>
string Add(T1 a, T2 b) {
	
	return to_string(a + b);
}
int main() {
	cout << Add(1, 2) << endl;
	cout << Add(1.1, 3);
}

3、模板类

​ 泛型的思想同样可以应用在类上。

template<typename T>
class A{
private:
	T data;
public:
    T getDate() {
        return data;
    }
    void setData(T data) {
        this->data = data;
    }
};
  • 模板参数也可以是一个实际的参数

  • 如 template<int size, typename T>,则我们可以在类中使用 size,也可以通过 <100, int> 这样的方式来指定 size 的值

  • 需要注意的是模板类的声明和定义必须放在一起

第八章 文件读写流

1、文件读写流

​ 回顾 cin 和 cout,这两个对象实现了标准的输入和输出,是在控制台中实现的,那要是想对文件进行读写应该如何操作呢?

  1. 标准读写引入的是 iostream 而文件读写引入的是 fstream,其中对应 cin 和 cout 我们也可以构造两个对象来进行文件的读写 fin 和 fout

    // 负责将文件读入程序
    std :: ifstream fin;
    // 负责将程序中的数据写入文件
    std :: ofstream fout;
    
  2. 案例

    #include<iostream>
    #include<string>
    #include<fstream>
    using namespace std;
    int main() {
    	ifstream fin;
    	ofstream fout;
    	fout.open("C:\\Users\\lenovo\\Desktop\\阿里云oss.txt", ios::out);
    	fout << "hello";
        fout.close();
    }
    
    #include<iostream>
    #include<string>
    #include<fstream>
    using namespace std;
    int main() {
    	string x;
    	ifstream fin;
    	fin.open("C:\\Users\\lenovo\\Desktop\\test.txt");
    	if (!fin.is_open()) {
    		cout << "打开失败";
    		return 1;
    	}
    	fin >> x;
    	cout << x;
    	fin.close();
    }
    

    文件打开模式

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2、二进制文件

​ 在正常使用是时候,为了节省空间,可以使用二进制文件

案例:

#include<iostream>
#include<string>
#include<fstream>
using namespace std;
int main() {
	ofstream fout;
	fout.open("C:\\Users\\lenovo\\Desktop\\test.txt", ios::app|ios::binary);
	if (!fout.is_open()) {
		cout << "打开失败";
		return 1;
	}
	int a = 999;
	fout.write((char*)&a, sizeof(int));
	fout.close();
}
#include<iostream>
#include<string>
#include<fstream>
using namespace std;
int main() {
	ifstream fin;
	fin.open("C:\\Users\\lenovo\\Desktop\\test.txt", ios::in|ios::binary);
	if (!fin.is_open()) {
		cout << "打开失败";
		return 1;
	}
	int a;
	fin.read((char*)&a, sizeof(int));
	fin.close();
	cout << a;
}

3、输出格式的控制

我们可以通过流格式控制符来实现输出格式的控制

std::setprecision(int precision):设置浮点数的精度,控制小数点后的位数。

cppCopy code#include <iostream>
#include <iomanip>

int main() {
    double value = 3.14159;
    std::cout << std::setprecision(3) << value << std::endl;  // 设置小数点后 3 位
    return 0;
}

常用的流格式控制符:

C++ 提供了流格式控制符,用于控制输入和输出的格式。以下是一些常用的流格式控制符:

  1. std::setw(int width):设置字段宽度,用于控制输出的字符数。

  2. std::setprecision(int precision):设置浮点数的精度,控制小数点后的位数。

  3. std::fixed:固定小数点表示,用于浮点数的输出。

  4. std::scientific:科学计数法表示,用于浮点数的输出。

  5. std::leftstd::right:控制文本的对齐方式,左对齐和右对齐。

  6. std::hexstd::dec:控制整数的输出进制,十六进制和十进制。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐