本文主要面向的是曾经学过、了解过C++的同学,旨在帮助这些同学唤醒C++的记忆,提升下自身的技术储备。如果之前完全没接触过C++,也可以整体了解下这门语言。

面向受众
本文主要面向的是曾经学过、了解过C++的同学,旨在帮助这些同学唤醒C++的记忆,提升下自身的技术储备。如果之前完全没接触过C++,也可以整体了解下这门语言。
C++是一种通用编程语言,它被广泛用于软件开发。C++以其强大的功能、高效的性能和灵活性而著称。以下是一些关键特点:
-
面向对象:C++支持面向对象编程(OOP)的四大特性:封装、继承、多态和抽象。通过类和对象,程序员能够创建模块化的代码,更容易地进行维护和扩展。
-
泛型编程:C++支持模板编程,允许编写与数据类型无关的代码。模板是实现泛型编程的关键工具,它们提高了代码的复用性。
-
直接内存管理:C++提供了对内存的直接操作能力,允许程序员手动管理内存分配和释放,这是C++的一个强大特性,也是需要谨慎使用的地方,因为不当的内存管理可能会导致资源泄露和其他问题。
-
性能:C++编写的程序通常有很高的执行效率,这是因为C++提供了与底层硬件直接对话的能力。这使得C++成为开发要求性能的系统软件(如操作系统、游戏引擎)的理想选择。
-
C语言兼容:大部分C语言程序可以在C++编译器上编译并运行。这一特性简化了从C到C++的过渡。
-
多编程范式支持:除了面向对象和泛型编程外,C++还支持过程式编程和函数式编程等范式,使其成为一个多样化的工具,能适应不同的编程需求。
C++语言的复杂劝退了很多人,诸如指针、虚函数、泛型等语言特性让C++变得特别复杂。事实也确实如此,不过C++的作者说过:“轻松地使用这种语言。不要觉得必须使用所有的特性,不要在第一次学习时就试图使用所有特性。”
本文主要内容是介绍现代C++(C++11及之后的版本)中的语法和特性,不会深入语法细节,每小节最后可能会列出一些相关的拓展知识点,感兴趣的同学可以自行了解。
语法基础
▐ 类型
C++是静态编译语言,所有变量在声明时都要指定具体的变量类型,或者能让编译器推导出具体的变量类型(比如使用 auto、decltype 关键字的场景),类型检查不通过将导致编译期出错。
-
基础类型
C++的基础类型可以按照其所能表示的数据类型来分类。以下表格列出了C++的基础类型及其常见的大小和范围(请注意,实际的大小和范围可能根据平台和编译器的不同而有所变化):
|
类别 |
类型 |
大小(位) |
备注 |
|
整形类型 |
short |
至少 16 |
|
|
unsigned short |
至少 16 |
||
|
int |
至少 16 |
通常是 32 |
|
|
unsigned int |
至少 16 |
||
|
long |
至少 32 |
||
|
unsigned long |
至少 32 |
||
|
long long |
至少 64 |
C++11 新增 |
|
|
unsigned long long |
至少 64 |
C++11 新增 |
|
|
定宽整数 (从 <cstdint> 导入) |
8/16/32/64 |
int8_t, int16_t, int32_t, int64_t 等 |
|
|
无符号定宽整数 (从 <cstdint> 导入) |
8/16/32/64 |
uint8_t, uint16_t, uint32_t, uint64_t 等 |
|
|
浮点数类型 |
float |
32 |
单精度浮点数 |
|
double |
64 |
双精度浮点数 |
|
|
long double |
实现依赖 |
扩展精度浮点数,精度和大小由具体实现定义 |
|
|
字符类型 |
char |
至少 8 |
可表示字符或小整数,有符号性由实现定义 |
|
signed char |
8 |
明确的有符号字符类型 |
|
|
unsigned char |
8 |
无符号字符类型 |
|
|
char16_t |
16 |
C++11 新增,用于 UTF-16 字符 |
|
|
char32_t |
32 |
C++11 新增,用于 UTF-32 字符 |
|
|
wchar_t |
实现依赖 |
用于宽字符集 |
|
|
布尔类型 |
bool |
实现依赖 |
表示布尔值 true 或 false |
|
特殊类型 |
void |
N/A |
表示无类型,用于函数返回值 |
|
nullptr_t |
指针宽度(32/64) |
C++11 新增,表示空指针 nullptr 的类型 |
|
|
自动类型 |
auto |
N/A |
C++11 新增,允许编译器自动推导变量类型 |
|
指针和引用类型 |
指针类型 |
指针宽度(32/64) |
例如 int* 表示整数指针 |
|
引用类型 |
一般和指针类型相同 |
例如 int& 表示整数引用 |
语法示例:
int a; // 声明未初始化,使用前建议手动初始化char b = 'a';float c = 1.0f; // C++中默认小数是double类型,加上f可以指定为floatdouble d = 2.0;auto e = 20; // 编译器自动推导auto为 int 类型
基础类型隐式转换
编译器自动进行的类型转换,不需要程序员进行任何操作。这些转换通常在类型兼容的情况下发生,比如从小的整数类型转换到大的整数类型。下面是经常遇到的隐式类型转换:
-
安全的隐式转换:
-
整型提升:小的整型(如 char、short)会自动转换成较大的整型(如 int)。
-
算术转换:例如,当 int 和 double 混合运算时,int 会转换为 double。
-
存在隐患的隐式转换:
-
窄化转换:大的整数类型转换到小的整数类型,或者浮点数转换到整数,可能会造成数据丢失或截断。
-
指针转换:例如,将 void* 转换为具体类型的指针时,如果转换不正确,会导致未定义行为。
-
结构体(struct)
结构体是不同类型数据的集合,允许将数据组织成有意义的组合。
语法示例:
// 结构体定义struct Person { std::string name;int age;}// 结构体初始化,Person person = {"Jim", 20};Person person2; // 创建另一个实例person2 = person;// 将person中的值复制到person2中,默认是浅拷贝,在有指针的情况下有潜在风险枚举(enum)
枚举是一种用户定义的类型,它可以赋予一组整数值具有更易读的别名。
语法示例:
enum Color { RED, GREEN, BLUE };// 使用Color myColor = RED;
C++11引入了新的枚举类型 作用域枚举。
语法示例:
enum class Color {RED,GREEN,BLUE};Color myColor = Color::RED; // 使用作用域解析运算符(::)访问枚举值
作用域枚举解决了传统枚举可能导致命名冲突的问题,并提供了更强的类型检查。
-
联合体(union)
联合体允许在相同的内存位置存储不同类型的数据,但一次只能使用其一。
语法示例:
// 联合体的定义union Data {int intValue;float floatValue;char charValue;}// 联合体一次只能保存一种类型的数据,每次赋值都会覆盖内存中之前的值// 因此联合体一般是配合结构体来使用,下面是一个示例// 定义数据类型的枚举enum DataType {INT,FLOAT,CHAR};// 定义一个结构体,它包含一个联合体和一个枚举标签struct SafeUnion {// 标记当前联合体中存储的数据类型DataType type;// 定义联合体union {int intValue;float floatValue;char charValue;} data;};// 赋值操作SafeUnion su;su.type = FLOAT;su.data.floatValue = 1.0f;// 使用时,通过type判断类型然后访问联合体对应的成员变量switch(su.type) {case FLOAT:cout << su.data.floatValue << endl;break;}
-
类(class)
类是C++的核心特性,是面向对象的基础,允许将数据和操作这些数据的函数封装为一个对象。这里先只介绍定义。
语法示例:
class Person {public:void doWork(); // 方法,类对外提供的一系列操作实例的函数private:std::string name; // 成员变量,封装到类中的属性,保存内部状态信息int age;};
-
列表初始化
现代C++提供了一种新的统一的变量初始化方式 – 列表初始化,推荐优先使用这种初始化方式,它能提供更加直观和统一的数据初始化方式。
列表初始化使用 {} 来初始化数据对象,包括基础类型、数组、结构体、类和容器等复杂的数据类型。语法示例:
// 基础类型int a{0};double b{3.14};// 结构体struct MyStruct {int x;double y;};MyStruct s{1, 2.0};// 类class MyClass {public:MyClass(int a, double b) : a_(a), b_(b) {}private:int a_;double b_;};MyClass obj{5, 3.14}; // MyClass 必须有一个匹配这个参数列表的构造函数// 数组int arr[3]{1, 2, 3};// 上面介绍的都是现代C++推荐写法,省略 =// 下面的2种写法绝大多数情况下是等价的float arr[2]{1, 2}; // 写法1float arr[2] = {1, 2}; // 写法2// 编译器对这两种写法的处理是一致的,方法2并不会产生临时变量和拷贝赋值,包括类的声明
现代C++推荐优先使用列表初始化来初始化变量,因为这种方式不允许进行窄化转换这能避免一些问题的发生,示例:
int a = 7.7; // 编译能通过,但是有warningint b = {1.0}; // 编译器拒绝通过,因为浮点到整形的转换会丢失精度
列表初始化支持参数列表小于数据对象的个数,这种情况下会默认进行其他变量的零初始化。
拓展:
-
零初始化
▐ 数组
C++的数组是一个固定大小的序列容器,它可以存储特定类型的元素的集合。数组中的元素在内存中连续存储,这允许快速的随机访问,即可以直接通过索引访问任何元素,而无需遍历数组。
-
数组的声明
数组的声明形式如下:
Typename arrayName[Size];// 基本类型int arr[10];char charArr[30];// 复杂类型struct Point {int x;int y;}Point points[10];
这里 Typename 是数组中元素的数据类型,arrayName 是数组的变量名,Size 是数组的元素个数,在这种声明形式下必须是整形的常量。
这里介绍的方式是数组的静态声明方式,即数组的元素个数在编译期间就能确定,数组占用的内存分配在栈内存中,实际开发中更多的情况可能是更具运行时的值确定数组的大小,这时需要动态的方式声明数组,后面会介绍。
-
数组的初始化
数组定义时如果未进行初始化,那么数组中的元素的值都是内存中残留的数据,而这些数据通常没有意义,直接使用会导致不可预知的问题。因此声明数组后需要对数组进行必要的初始化。
数组支持列表初始化语法:
int arr[] = {1, 2, 3, 4, 5}; // 数组大小为5,编译器自动确定int arr[10] = {1, 2, 3}; // 数组前三项确定为1,2,3,其余被初始化为0int arr[10] = {0}; // 整个数组全部为0
-
数组的使用
数组中的元素可以通过索引来访问和修改,索引从0开始,第一元素索引是0,最后一个索引是Size-1。
int arr[10] = {}; // 零初始化arr[0] = 10; // 修改数组第一个元素值为10
-
数组在声明后(无论静态声明还是动态声明),数组的大小即固定,不可更改;
-
数组不提供任何内置的方法来获取其大小,通常需要额外保存数组的大小,或者使用特殊标记结束元素(C风格的字符串使用’