c++学习过程
相关链接
菜鸟教程 - C++ 教程
在线编译器
「C++学习网」
Learn C++
Git速查手册
cppreference 《=》cppreference 中文
cppreference 是一个免费学习 C++ 的网站,你也可以把它看成是一个 C++ 学习手册,内容相当丰富,涵盖几乎所有 C++ 的知识点,除此以外,它内容更新很快,紧随 C++ 标准,目前已经到 C++23 的内容了。
除了知识点以外,cppreference 还提供了丰富的示例,给出容易理解且可能的实现,更易于大家在学习的时候去理解知识点。
章节学习
C++ 环境设置
使用 VsCode 运行 c++ 程序 - 注意:操作设置完成后需要重启电脑才好使
C++ AI 编程助手
C++基础
C++ 基本语法
C++ 数据类型
C++ 变量类型
C++ 变量作用域
- 变量的创建和销毁发生在程序运行时(称为运行时),而不是在编译时。生命周期是一个运行时属性,作用域是一个编译时属性。
- 作用域是编译时属性,使用不在作用域的变量将导致编译错误。
- 生命周期,代表变量实例化后的实例的存活时间。局部变量实例的生命周期在其超出范围的点结束,因此局部变量在此时被销毁。
- 生命周期内的变量,不一定在对应代码的作用域内。
注意,并不是所有类型的变量实例都在超出作用域时被销毁。
C++ 常量
C++ 修饰符类型
C++ 存储类
C++ 运算符
C++ 循环
C++ 判断
C++ 函数
- 为了编写函数的前向声明,使用函数声明语句(也称为函数原型)。函数声明由函数的返回类型、名称和参数类型组成,以分号结尾。参数名称是可选的,声明中不包括函数体。
函数声明与定义:
- 声明告诉编译器标识符的存在及其关联的类型信息。
- 定义是一个声明,它实际实现(对于函数和类型)或实例化(对于变量)标识符。
Lambda 函数与表达式
C++ 数字
C++ 数组
C++ 字符串
C++ 指针
C++ 引用
C++ 日期 & 时间
C++ 基本的输入输出
C++ 结构体(struct)
C++ vector 容器
C++ 数据结构
- 数组(Array)
- 结构体(Struct)
- 类(Class)
- 链表(Linked List)
- 栈(Stack)
- 队列(Queue)
- 双端队列(Deque)
- 哈希表(Hash Table)
- 映射(Map)
- 集合(Set)
- 动态数组(Vector)
面向对象
C++ 类 & 对象
类是 C++ 中用于面向对象编程的核心结构,允许定义成员变量和成员函数。与 struct 类似,但功能更强大,支持继承、封装、多态等特性。
特点:
- 可以包含成员变量、成员函数、构造函数、析构函数。
- 支持面向对象特性,如封装、继承、多态。
类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。
类成员函数
类访问修饰符
构造函数 & 析构函数
C++ 拷贝构造函数
在C++中,拷贝构造函数是一种特殊的构造函数,它用于创建一个对象作为另一个同类型对象的副本。拷贝构造函数通常在以下几种情况下被调用:
- 当你用一个对象来初始化同类型的另一个对象时。
- 当你将一个对象作为参数传递给一个函数,而该函数需要一个该类型的对象时。
- 当你从函数返回一个对象时。
- 当你用一个对象去初始化一个数组或容器的元素时。
拷贝构造函数的基本形式如下:
class ClassName {
public:
// 拷贝构造函数
ClassName(const ClassName& other) {
// 使用other来初始化当前对象
}
};
默认拷贝构造函数
如果你没有为你的类显式定义拷贝构造函数,编译器会为你生成一个默认的拷贝构造函数。默认的拷贝构造函数逐成员地拷贝对象的数据成员。例如:
class Example {
public:
int value;
// 默认拷贝构造函数,编译器会生成这个函数
Example(const Example& other) {
value = other.value;
}
};
自定义拷贝构造函数
如果你需要自定义拷贝构造函数的行为(例如,深拷贝而不是浅拷贝),你可以自己定义它:
class Resource {
public:
int* data; // 指向动态分配内存的指针
Resource(int value) : data(new int(value)) {} // 构造函数
// 自定义拷贝构造函数
Resource(const Resource& other) : data(new int(*other.data)) {} // 深拷贝
~Resource() { delete data; } // 析构函数释放资源
};
禁用拷贝构造函数
在某些情况下,你可能不希望你的类对象被拷贝。例如,如果一个对象拥有资源(如文件句柄、网络连接等),你可能不希望在多个地方有多个对象的副本指向同一个资源。你可以通过删除(delete)拷贝构造函数来禁止拷贝:
class NonCopyable {
public:
NonCopyable() = default; // 默认构造函数仍然可用
NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数,禁止拷贝
};
移动构造函数(C++11及以后)
在C++11及以后的版本中,还引入了移动构造函数,它用于在需要时将资源的所有权从一个对象转移到另一个对象,通常用于优化资源移动而不是复制的开销。例如:
class MovableResource {
public:
std::unique_ptr<int> data; // 使用unique_ptr自动管理资源
MovableResource(int value) : data(new int(value)) {} // 构造函数
// 移动构造函数
MovableResource(MovableResource&& other) noexcept : data(std::move(other.data)) {} // 转移资源的所有权
};
通过合理使用拷贝构造函数、移动构造函数以及相关的资源管理技术,你可以更有效地控制C++中的资源管理,避免内存泄漏和不必要的复制开销。
C++ 友元函数
C++ 内联函数
C++ 中的 this 指针
C++ 中指向类的指针
C++ 类的静态成员
C++ 重载运算符和重载函数
一元运算符重载
二元运算符重载
【C++】运算符重载 ③ ( 二元运算符重载 | 运算符重载步骤 | 全局函数 实现 运算符重载 | 成员函数 实现 运算符重载 | 友元函数 实现 运算符重载 )
C++运算符重载——重载二元运算符
关系运算符重载
输入/输出运算符重载
++ 和 – 运算符重载
赋值运算符重载
函数调用运算符 () 重载
下标运算符 [] 重载
类成员访问运算符 -> 重载
C++ 多态
多态按字面的意思就是多种形态。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
在 C++ 中,多态(Polymorphism)是面向对象编程的重要特性之一。
C++ 多态允许使用基类指针或引用来调用子类的重写方法,从而使得同一接口可以表现不同的行为。
多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。
以下是多态的几个关键点:
- 虚函数(Virtual Functions):
在基类中声明一个函数为虚函数,使用关键字virtual。
派生类可以重写(override)这个虚函数。
调用虚函数时,会根据对象的实际类型来决定调用哪个版本的函数。 - 动态绑定(Dynamic Binding):
也称为晚期绑定(Late Binding),在运行时确定函数调用的具体实现。
需要使用指向基类的指针或引用来调用虚函数,编译器在运行时根据对象的实际类型来决定调用哪个函数。 - 纯虚函数(Pure Virtual Functions):
一个包含纯虚函数的类被称为抽象类(Abstract Class),它不能被直接实例化。
纯虚函数没有函数体,声明时使用= 0。
它强制派生类提供具体的实现。
多态的实现机制:
虚函数表(V-Table):C++运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表,表中存储了指向类中所有虚函数的指针。
虚函数指针(V-Ptr):对象中包含一个指向该类虚函数表的指针。
使用多态的优势:
代码复用:通过基类指针或引用,可以操作不同类型的派生类对象,实现代码的复用。
扩展性:新增派生类时,不需要修改依赖于基类的代码,只需要确保新类正确重写了虚函数。
解耦:多态允许程序设计更加模块化,降低类之间的耦合度。
注意事项:
只有通过基类的指针或引用调用虚函数时,才会发生多态。
如果直接使用派生类的对象调用函数,那么调用的是派生类中的版本,而不是基类中的版本。
多态性需要运行时类型信息(RTTI),这可能会增加程序的开销。
注意(个人理解多态与重写)
:在基类中写一个虚函数,在派生类中重新实现这个函数即为 重写
;在主函数中需要定义了一个基类指针,这个指针可以指向任何基类的对象或其派生类的对象。在通过基类的指针或引用调用派生类中重写的虚函数时,这个过程即为 多态
。同时,调用虚函数时具体虚函数的内容会根据派生类的对象动态绑定到派生类重写虚函数所实现的内容;
关键概念:
-
虚函数:在基类 Shape 中定义了虚函数 area()。虚函数的作用是让派生类可以重写此函数,并在运行时根据指针的实际对象类型调用适当的函数实现。
虚函数是在基类中使用关键字 virtual 声明的函数。虚函数允许子类重写它,从而在运行时通过基类指针或引用调用子类的重写版本,实现动态绑定。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
特点:
1、在基类中可以有实现。通常虚函数在基类中提供默认实现,但子类可以选择重写。
2、动态绑定:在运行时根据对象的实际类型调用相应的函数版本。
3、可选重写:派生类可以选择性地重写虚函数,但不是必须。 -
纯虚函数:
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
纯虚函数是没有实现的虚函数,在基类中用 = 0 来声明。纯虚函数表示基类定义了一个接口,但具体实现由派生类负责。纯虚函数使得基类变为抽象类(abstract class),无法实例化。
特点:
1、必须在基类中声明为 = 0,表示没有实现,子类必须重写。
2、抽象类:包含纯虚函数的类不能直接实例化,必须通过派生类实现所有纯虚函数才能创建对象。
3、接口定义:纯虚函数通常用于定义接口,让派生类实现具体行为。
= 0
告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
一定要区别于重写(需要 override 关键字)
以及实现(直接实现一个正常的函数即可)
-
动态绑定:因为 area() 是虚函数,shape->area() 调用时会在运行时根据 shape 实际指向的对象类型(Rectangle 或 Triangle)来调用相应的 area() 实现。这种在运行时决定调用哪个函数的机制称为动态绑定,是多态的核心。
-
基类指针的多态性:基类指针 shape 可以指向任何派生自 Shape 的对象。当 shape 指向不同的派生类对象时,调用 shape->area() 会产生不同的行为,这体现了多态的特性。
C++ 数据抽象
C++ 数据封装
数据封装(Data Encapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。
把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。理想的做法是尽可能地对外隐藏每个类的实现细节。
访问修饰符:
- private: 私有成员只能在类的内部访问,不能被类的外部代码直接访问。
- public: 公有成员可以被类的外部代码直接访问。
- protected: 受保护成员可以被类和其派生类访问。
数据封装通过类和访问修饰符(public, private, protected)来实现.
C++ 接口(抽象类)
接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类
。
设计策略
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
C++ 文件和流
C++ 异常处理
C++ 动态内存
了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的内存分为两个部分:
- 栈:在函数内部声明的所有变量都将占用栈内存。
- 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
很多时候,您无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。
在 C++ 中,您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。
如果您不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。
malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。
在任何时候,当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 操作符释放它所占用的内存。
命名空间
随着程序变得更大使用更多标识符,引入命名冲突的几率显著增加。好消息是C++提供了大量避免命名冲突的机制。局部作用域就是这样一种机制,可以防止不同函数内定义的局部变量相互冲突。但局部范围不适用于函数名。那么如何防止函数名相互冲突呢?
C++ 模板
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector 或 vector 。您可以使用模板来定义函数和类。
函数模板
模板函数定义的一般形式如下所示:
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}
在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
类模板
正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:
template <class type> class class-name {
.
.
.
}
在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。
C++ 预处理器
#define 预处理
#define 预处理指令用于创建符号常量。该符号常量通常称为宏。
参数宏
您可以使用 #define 来定义一个带有参数的宏。
条件编译
有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。
条件预处理器的结构与 if 选择结构很像。
# 和 ## 运算符
#
和 ##
预处理运算符在 C++ 和 ANSI/ISO C 中都是可用的。#
运算符会把 replacement-text 令牌转换为用引号引起来的字符串。##
运算符用于连接两个令牌。
C++ 中的预定义宏
C++ 提供了下表所示的一些预定义宏:
C++ 信号处理
信号是由操作系统传给进程的中断,会提早终止一个程序。
有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal>
中。
C++ 多线程
线程是程序中的轻量级执行单元,允许程序同时执行多个任务。多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。
一般情况下,两种类型的多任务处理:基于进程和基于线程。
- 基于进程的多任务处理是程序的并发执行。
- 基于线程的多任务处理是同一程序的片段的并发执行。
C++ 多线程编程涉及在一个程序中创建和管理多个并发执行的线程。C++ 提供了强大的多线程支持,特别是在 C++11 标准及其之后,通过 标准库使得多线程编程变得更加简单和安全。
创建线程
C++ 11 之后添加了新的标准线程库 std::thread,std::thread 在 头文件中声明,因此使用 std::thread 时需要包含 在 头文件。
std::thread
#include<thread>
std::thread thread_object(callable, args...);
- callable:可调用对象,可以是函数指针、函数对象、Lambda 表达式等。
- args…:传递给 callable 的参数列表。
C++ Web 编程
C++ 资源库
C++ STL 教程
C++ 标准模板库(Standard Template Library,STL)是一套功能强大的 C++ 模板类和函数的集合,它提供了一系列通用的、可复用的算法和数据结构。
STL 的设计基于泛型编程,这意味着使用模板可以编写出独立于任何特定数据类型的代码。
STL 分为多个组件,包括容器(Containers)、迭代器(Iterators)、算法(Algorithms)、函数对象(Function Objects)和适配器(Adapters)等。
C++ 标准模板库的核心包括以下重要组件组件:
C++ 导入标准库
在 C++ 编程中,标准库(Standard Library)是一个非常重要的组成部分。
标准库提供了大量的预定义函数和类,可以帮助我们更高效地完成各种任务,为了使用这些功能,我们需要在程序中导入相应的标准库头文件。
使用 #include 包含头文件
在 C++ 中,我们使用 #include 预处理指令来导入标准库头文件。
#include 指令告诉编译器在编译时将指定的头文件内容插入到当前文件中,头文件通常包含函数声明、类定义、宏定义等内容。
#include <iostream> // 导入输入输出流库
#include <vector> // 导入向量容器库
#include <cmath> // 导入数学函数库
三个常用的标准库头文件:
<iostream>
:提供了输入输出流的功能,如std::cout和std::cin。<vector>
:提供了向量容器的实现,用于存储动态数组。<cmath>
:提供了常用的数学函数,如sqrt()、sin()、cos()等。
C++ 标准库
C++ 有用的资源
C++ 实例
小知识点总结
1、.
和 ->
运算符具体使用场景(用于访问结构体/类的成员)
.
(点号)
使用场景 :直接访问 对象本身 的成员。
struct Student {
int age;
};
Student s;
s.age = 20; // 通过对象 s 访问成员 age
->
(箭头)
使用场景 :通过 指针访问对象的成员 ,等价于 (*指针).成员。
struct Student {
int age;
};
Student* p = new Student();
p->age = 20; // 等价于 (*p).age = 20;
- 嵌套使用
复杂结构 :当结构体成员本身是结构体或指针时,需结合使用:
struct Address {
int zip;
};
struct Person {
Address addr; // 成员是结构体
Address* pAddr; // 成员是指针
};
Person p;
p.addr.zip = 10000; // 通过对象访问嵌套结构体成员
p.pAddr->zip = 20000; // 通过指针访问嵌套结构体成员
关键区别
注意事项
优先级问题 :->
的优先级高于 *
,但 .
的优先级高于 *
,因此需注意表达式结合顺序:
*p.addr; // 等价于 *(p.addr):先访问 addr 成员,再解引用
(*p).addr; // 显式优先级控制
p->addr; // 推荐写法,清晰直观
通过区分对象与指针的使用场景,可以避免混淆并正确访问结构体/类成员。