C 语言是一种通用的、过程式的编程语言,广泛用于系统软件和应用程序开发。C++ 进一步扩充和完善了 C 语言,是一种面向对象的程序设计语言。
https://en.cppreference.com/w/
https://www.studycpp.cn
基础知识
程序结构
以下代码是最简单的 C++程序之一,它将帮助我们理解 C++程序的基本语法结构。
// Header file for input output functions |
- C++ 程序按照代码的书写顺序执行,其中
main()
函数是每个程序的入口点,且只有一个main
函数。 - 语句块是一组使用大括号
{}
括起来的按逻辑连接的语句。 - 每个语句必须以分号结束。
- 上例中
#include<iostream>
是一个预处理器指令,告诉编译器引入 C++标准输入/输出库iostream
。 using namespace std
将 std 命名空间的实体导入到程序中,它基本上是定义所有内置函数的空间。
编译和运行
常用的编译器
- GCC 全称 GNU Compiler Collection,是由 GUN 项目开发的编译器套件。包含了 C、C++、Objective-C、Fortran、Ada 和 Go 等多种高级编程语言的编译器。
- Clang 用于编译 C、C++、Objective-C 和 Objective-C++的编译器前段。采用 LLVM 为后端,一般 MacOS 下使用较多。
- MSVC Microsoft 开发的 C 和 C++编译器,Windows 系统使用较多。
大多数的 C++ 编译器并不在乎源文件的扩展名,一般默认使用 .cpp。
g++ helloworld.cpp -o hello |
注释
C++有两种注释类型:单行注释 //
和多行注释 /*...*/
|
数据类型与变量
数据类型
C++ 是一门静态类型语言。这意味着任何变量都有一个相关联的类型,并且该类型在编译时是可知的。
- 基本数据类型:
int
,float
,double
,char
,bool
,void
- 派生数据类型:数组(array)、指针(pointer)、引用(reference)、函数(function)、结构体(
struct
)、共用体(union
)、枚举(enum
)、类(class
) - 数据类型修饰符:
short
,long
,signed
,unsigned
不同的数据类型也有不同的范围,这些范围可能因编译器而异。数据类型修饰符可作为前缀用于修改已有数据类型可以存储的数据大小或范围。以下是32 位 GCC 编译器上的范围列表以及内存要求和格式指定符。
Data Type | Memory (bytes) | Range | Format Specifier |
---|---|---|---|
short int |
2 | -215 to 215-1 | %hd |
unsigned short int |
2 | 0 to 216-1 | %hu |
unsigned int |
4 | 0 to 232-1 | %u |
int |
4 | -231 to 231-1 | %d |
long int |
4 | -231 to 231-1 | %ld |
unsigned long int |
4 | 0 to 232-1 | %lu |
long long int |
8 | -(263) to 263-1 | %lld |
unsigned long long int |
8 | 0 to 264 | %llu |
float |
4 | 1.2E-38 to 3.4E+38 | %f |
double |
8 | 1.7E-308 to 1.7E+308 | %lf |
long double |
16 | 3.4E-4932 to 1.1E+4932 | %Lf |
signed char |
1 | -128 to 127 | %c |
char |
1 | 0 to 255 | %c |
定义变量
当变量被定义时,它就会分配内存。分配的内存量取决于变量打算存储的数据类型。
变量本质是一块内存区域的别名,用来存储程序运行时需要的数据。变量在使用前必须声明,声明时指定数据类型,例如
int age; // Defining a variable |
也可同时声明和赋值变量
// Defining and initializing a variable |
注意,char
类型使用单引号,用于存储单个字符。
C/C++ 允许一次创建多个变量
char a = 'A', b; |
C++11 新增 auto
关键字,用于自动推断数据类型
auto age = 18; |
常量和宏
使用const
关键字声明常量
const int MAX_SIZE = 100; |
也可以使用宏定义#define
来表示常量
为了和变量区分开来,常量名通常全部大写。
命名空间
为防止命名冲突,C++引入了命名空间。以关键字 namespace
开头,后跟命名空间名称。
|
注意,大括号后没有分号。
为了调用带有命名空间的函数或变量,需要用域解析操作符 ::
来指明要使用的命名空间
int main (){ |
还可以采用 using
关键字引入整个命名空间,这样在使用命名空间时就可以不用加上前缀,如
using std; |
using
指令也可以用来指定命名空间中的特定项目:
using std::cout; |
随后的代码中,在使用 cout
时就可以不用加上命名空间名称作为前缀。
不连续的命名空间:可以创建两个具有相同名称的命名空间块。第二个命名空间块实际上只不过是第一个命名空间的延续。因此,我们可以将一个命名空间的各个组成部分分散在多个文件中,例如 std
命名空间,C++标准库中的函数和类通常都位于 std
命名空间中。
运算符
在 C++中,运算符根据其执行的操作类型分为 6 种类型。
算术运算符
算术运算符用于执行算术或数学运算。
Operator | Description | Syntax |
---|---|---|
+ |
Plus | a + b |
– |
Minus | a – b |
* |
Multiply | a * b |
/ |
Divide | a / b |
% |
Modulus | a % b |
++ |
Increment | a++ |
-- |
Decrement | a-- |
a++
与 ++a
都是增量运算符,但是,两者都略有不同:a++
在使用 a
之后才自增它的值,而 ++a
会在使用 a
之前自增它的值。递减运算符也会发生类似的情况。
关系运算符
关系运算符用于比较两个数的值,返回 0 (false) 或 1 (true)
Operator | Description | Syntax |
---|---|---|
< |
Less than | a < b |
> |
Greater than | a > b |
<= |
Less than or equal to | a <= b |
>= |
Greater than or equal to | a >= b |
== |
Equal to | a == b |
!= |
Not equal to | a != b |
浮点数的精度是有限的,因此在比较两个浮点数是否相等时,应该使用一个小的误差范围来判断,而不是直接使用 ==
操作符。
逻辑运算符
逻辑运算符用于组合两个或两个以上条件,结果返回一个布尔值。
Operator | Description | Syntax |
---|---|---|
&& |
Logical AND | a && b |
|| | Logical OR | a || b |
! |
Logical NOT | !a |
按位运算符
Operator | Description | Syntax |
---|---|---|
& |
Bitwise AND | a & b |
| | Bitwise OR | a | b |
^ |
Bitwise XOR | a ^ b |
~ |
Bitwise First Complement | ~a |
<< |
Bitwise Leftshift | a << b |
>> |
Bitwise Rightshilft | a >> b |
注意:只有 char 和 int 数据类型可以与 Bitwise 运算符一起使用。
赋值运算符
Operator | Description | Syntax |
---|---|---|
= |
Simple Assignment | a = b |
+= |
Plus and assign | a += b |
-= |
Minus and assign | a -= b |
*= |
Multiply and assign | a *= b |
/= |
Divide and assign | a /= b |
%= |
Modulus and assign | a %= b |
三元运算符
条件运算符也被称为三元运算符
(expression) ? value_if_true : value_if_false; |
?
运算符首先检查给定的条件,如果条件为真,则执行第一个表达式,否则执行第二个表达式。它是 C++中 if-else 条件的替代方案。
|
sizeof
sizeof
是一个运算符,它以字节为单位计算变量或者数据类型的大小。
|
静态转换
注意,整数运算只能得到结果的整数部分
int a = 1, b = 3; |
因此需要强制转换类型
cout << static_cast<float>(a) / b; // 0.3333333 |
静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。
标准输入输出
cstdio
<cstdio>
是 C++ 标准库中的一个头文件,它包含了 C 语言标准 I/O 库的 C++ 封装,主要用于文件的输入和输出操作。我们通常会使用 scanf
和 printf
函数进行标准输入输出。
|
printf()
格式控制符的完整形式如下:
%[flag][width][.precision]type |
type 表示输出类型,width 表示最小输出宽度,当输出结果的宽度不足时,默认会在左边补齐空格。precision 表示输出精度,也就是小数的位数。用于整数时,precision 表示最小输出宽度,整数的宽度不足时会在左边补 0,用于字符串时,precision 表示最大输出宽度。
iostream
<iostream>
库是 C++ 标准库中用于输入输出操作的头文件。其中定义了几个常用的流类和操作符:
std::cin
标准输入流std::cout
标准输出流std::cerr
非缓冲标准错误流std::clog
缓冲标准日志流。
同时,重载的输入输出运算符 <<
和 >>
可以自行分析所处理的数据类型,无需像使用 scanf
和 printf
函数那样给出格式控制字符串。
|
其中 endl
用于在行末添加一个换行符 \n
。
注意,在使用 cin
将文本作为输入时,一旦遇到空格、制表符或换行符就会停止读取输入。使用标准库 <string>
中的 getline
函数可以读取包含空格的整行输入。
|
流程控制
条件语句
if-else
|
switch-case
在 C++中,当主要根据变量或表达式的值来评估多种情况时,就会使用 switch-case
|
注意
case
语句并没有花括号{}
,如果遗漏了break
,后续语句将全部执行,直到遇到break
语句。
循环语句
while
int sum = 0; |
do-while
int sum = 0; |
for
for
循环会先初始化计数器,然后,在每次循环前检测循环条件,在每次循环后更新计数器。
for (int i = 0; i <= 100; i++) { |
还有一种基于范围的 for
循环,不需要条件和更新语句。它只能用于可迭代的对象,如向量、集合等。
vector<int> v { 1, 2, 3, 4, 5}; |
无限循环
// This is an infinite for loop as the condition expression is blank |
跳转语句
break
break
语句用于完全终止循环或 switch
语句
|
continue
continue
语句用于跳过当前迭代并继续下一个迭代
|
指针和引用
指针
在讲解指针之前,先来了解下内存(memory)的概念。内存是最重要的硬件之一,是用来存储数据的。内存的最小单位是字节(byte,1byte=8bit),每个字节都有一个唯一的地址(类似储物柜的编号),程序可以通过这个地址访问内存中的数据。
指针(Pointer)是一个存储内存地址的变量,它所存储的值是内存中某个位置的地址。使用指针的主要原因是操作方便、效率高。
我们可以使用引用运算符 &
获取内存中该变量的地址,将该地址赋给一个指针变量。声明指针变量的语法不同,需要在其名称前加上星号 *
,指针变量的类型是其指向的的变量类型。
int age; |
指针的解引用运算符 *
可以获取和修改该地址指向的变量的值。注意,解引用运算符和声明指针时前面加的星号含义是不同的。
|
Output
Value at ptr = 0x7ffe454c08cc |
指针运算
指针的 +/-
运算相当于移动指针,常用在数组中。
int a[10], *ptr; |
两指针的差值为指针相隔元素的个数。
指针的指针
由于指针也是变量,也有存储地址,同样也可以定义另一个指针指向该指针,称为指针的指针。
int **ptr2 = &ptr; |
空指针
在指针变量声明的时候,如果没有确切的地址可以赋值,建议赋值为 NULL 。NULL 指针是一个定义在标准库中的值为零的常量。
int *ptr1 = 0; |
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。如需检查一个空指针,可以使用 if 语句:
if(ptr) |
引用
引用(Reference)是 C++ 相对于 C 语言的又一个扩充。引用可以看做是同一份内存的别名。引用的声明方式类似于指针,在变量名前加上 &
。引用必须在定义的同时初始化,并且以后也不能再引用其它数据。
|
注意,引用不需要运算符 *
即可访问值。它们可以像普通变量一样使用。仅在声明时需要 &
运算符。
我们在循环中使用引用来修改所有元素
|
派生数据类型
数组
声明和初始化
在 C++ 中,数组是一种数据结构,用于将相同数据类型存储在连续的内存位置。
在 C++ 中,我们只需先指定数据类型,然后指定数组的名称及其大小即可声明数组。不允许使用变量定义数组大小,数组一旦创建后,大小就不可改变。
你可以在声明数组之后为其赋值
int arr[5]; |
也可以在声明数组的时候进行初始化
int arr[5] = {1, 2, 3, 4, 5}; |
如果我们已经用值初始化了数组,但没有声明数组的长度,则数组的长度等于大括号内的元素数量。
我们也可以初始化部分数组,其余元素为默认值:整型都是0
,浮点型是0.0
,布尔型是false
。
int arr[5] = {2, 4, 8, 12, 16}; |
在 C++ 中,我们没有像 Java 中那样的 length 函数来查找数组大小,但我们可以使用 sizeof
运算符计算数组的大小。
// Length of an array |
数组与指针
数组可以通过索引来访问、修改元素,索引从0
开始。
arr[0] = 1; |
在 C++ 中,数组和指针彼此密切相关。数组名其实是一个指针常量,它存储的是数组中首个元素的地址,即 arr
和 &arr[0]
是等价的。因此,可以像普通指针一样使用数组名。
// C++ Program to Illustrate that Array Name is a Pointer |
Output
Memory address of arr: 0x7fff2f2cabb0 |
上例中,我们能够将 arr
分配给 ptr
,因为 arr
也是一个指针。之后,我们使用引用运算符 &
打印 arr
的内存地址,并打印存储在指针 ptr
中的地址,我们可以看到 arr
和 ptr
,它们都存储相同的内存地址。
现在,我们可以仅使用数组名称访问数组的元素,即 *(arr + i)
等价于 arr[i]
int arr[] = {2, 4, 8, 12, 16}; |
多维数组
使用最广泛的多维数组是 2D 数组和 3D 数组。这些数组通常以行和列的形式表示。
int arr[3][4] = { |
访问二维数组的某个元素需要使用行和列两个索引: array[row][col]
。
二维数组由多个一位数组组成,数组名 arr
代表二维数组首地址,也代表第 0 行首地址,arr + 1
代表第 1 行首地址,依次类推。一般 arr[i] + j
代表第i
行第j
列元素地址,即 &arr[i][j]
。
字符串
C 语言中字符串并不是一种基本数据,实际上是一个以 \0
结尾的字符数组。以下是 C 中定义的字符串的内存表示:
可以像初始化一个普通的数组那样初始化一个字符串,或者使用更加方便的字符串常量表示
char str[] = { 'H', 'e', 'l', 'l', 'o', '\0' }; |
字符串使用双引号表示时,编译器会在初始化时自动追加一个
\0
C++ 除了可以使用 C 风格的字符串,还可以使用标准库 <string>
中的 std::string
类。它是对 C 风格字符串的封装,提供了更安全、更易用的字符串操作功能。
|
string 字符串也可以像 C 风格的字符串一样按照下标来访问其中的每一个字符,起始下标仍是从 0 开始。
cout << "First character: " << str[0]; |
支持使用 +
或 +=
运算符来直接拼接 string 字符串
string str1 = "Hello, "; |
结构体
定义和使用
结构体是一种自定义的数据类型,用于创建复杂的数据结构,他可以包含多个不同类型的成员。
struct [tag] { |
可以先定义结构体类型,再声明结构体变量;
struct Student { |
也可以直接声明变量,同时省略结构体名
struct { |
可以在声明的时候初始化一个结构体:
struct Student Alice = { 18, "Alice", 'F' }; |
结构体通过点语法来访问和修改成员:
printf("%s, age %d", Alice.name, Alice.age); |
内嵌结构体
struct Student { |
初始化
// age, name, sex, year, month, day, score[0], score[1], score[2] |
结构体指针
像原始类型一样,我们可以有指向结构体的指针。
struct Student* ptr = &Alice; |
结构体指针必须使用 ->
运算符访问结构体的成员:
ptr->age = 20; |
结构体数组
与其他原始数据类型一样,我们可以创建一个结构体数组。
struct Student stu[5]; |
枚举
枚举 (Enumerated ) 是用户定义的数据类型,可以为其分配一些有限的值。这些值由用户在声明时定义。
|
在 C/C++ 中,将枚举值作为 int
连续值来处理。默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。但是,您也可以给名称赋予一个特殊的值。
类型别名
C/C++ 语言允许使用 typeof
关键字对数据类型赋予一个新名字
typedef char[] STRING; |
使用 typedef
,我们可以简化处理结构体时的代码。声明变量的时候不需要 struct 关键字
typedef struct { |
C++11 引入 using 关键字为现有类型定义别名
using char[] = STRING; |
函数
函数允许用户将程序划分为多个模块,每个模块执行特定任务。
定义和调用
函数定义包括返回类型、函数名、参数列表和函数体。
|
上例中 int
为函数返回值类型,如无返回值,以 void
类型表示,函数返回值用 return
显示给出。如无返回值,我们仍然可以使用 return
语句终止函数。
前向声明
由于 C++ 程序按照代码的书写顺序执行,因此在函数调用之前必须先声明。函数声明告诉编译器参数的数量、参数的数据类型以及返回函数的类型。
void hello(); |
在函数声明的时候可以只写参数类型,省略参数名
int sum(int a, int b); // Function declaration with parameter names |
参数传递
函数在定义时预期接收的参数称为形式参数,函数调用时实际传入的参数称为实际参数。C++ 支持三种参数传递方法:
- 值传递:函数调用的时候会把实参的值拷贝一份传给函数内部,并不会影响到函数外部。C++默认方法。
- 指针传递:函数调用的时候会把实参的地址传递给函数,在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
- 引用传递:函数调用的时候传递实参的引用。在函数内,对引用的操作会直接作用于实际参数。
|
Output
Before swap: x = 10, y = 20 |
在 C++ 中,数组名实质上是指针,数组参数在传递的函数中都被视为指针。有两种数组参数传递方式:
- 将数组名称作为指针参数传递, 如
int* arr
- 函数使用简单的数组声明接受数组,如
int arr[]
|
默认参数
在 C++ 中,定义函数时可以给形参指定一个默认的值,默认参数只能放在形参列表的最后。
|
如果函数是单独声明和定义的,则参数的默认值必须在声明中,且声明默认参数后,无法在函数定义中修改它们。
// Declaration with default argument |
命令行参数
在 C++ 程序中,命令行参数是使用 main()
函数参数来接收的。
int main (int argc, char* argv[]) |
其中,argc
是指传入参数的个数,argv[]
是一个指针数组,指向传递给程序的每个参数。
|
应当指出的是,argv[0]
存储程序的名称,如果没有提供任何参数,argc
将为 1。多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号或单引号内部。
返回指针的函数
|
递归函数
函数直接或间接调用自身,直到满足给定条件。
// calculate the sum of first N natural numbers using recursion |
函数指针
在 C 语言中,函数其实也可以看作一种数据类型,函数的类型其实就是它的返回值和参数列表。我们可以定义一个函数指针
void (*ptr)(int*, int*); |
这里的 ptr
就是一个函数指针, *ptr
就代表该函数。
*ptr(&x, &y); |
函数指针的应用也是非常广泛的,比如在实现一个回调函数的时候,就可以把函数指针作为参数传递给另一个函数,然后在这个函数里使用函数指针来使用函数,这种方式可以让代码更加灵活。
这里,我们有 c++ 示例,用于访问数组中的元素
|
函数重载
C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)。调用时根据实参和形参的匹配选择最佳函数
|
模板函数
C++ 支持将数据类型作为参数传递,这样我们就不需要为不同的数据类型编写相同的代码。这个函数就称为函数模板(Function Template)。
template <typename T> |
template
和 typename
是定义函数模板的关键字,它后面尖括号里的 T
是类型占位符。typename T
告诉编译器:字母 T 将在接下来的函数里代表一种不确定的数据类型。
|
就像普通参数一样,我们可以将多个数据类型作为参数传递给模板,也可以为模板指定默认参数
// C++ Program to implement use of template |
lambda 表达式
C++ 11 引入了 lambda 表达式。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
[capture] (parameters) -> return-type{body} |
通常,lambda 表达式中的 retur-type 由编译器本身计算,我们不需要显式指定它。
[](int a, int b){ return (a < b) ? b : a ; } |
[]
方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数体中可以使用的外部变量。
Syntax | Description |
---|---|
[] |
空方括号表示不导入任何外部变量 |
[=] |
表示以值传递的方式导入所有外部变量 |
[&] |
表示以引用传递的方式导入所有外部变量 |
[x, &y] |
x 以传值方式导入,y 以引用方式导入 |
[&, x] |
x 以值传递方式导入,其余变量以引用方式导入 |
[=, &x] |
x 以引用方式导入,其余变量以值传递方式导入 |
小括号可以接收外部传递的多个参数,和普通函数不同的是,如果不需要传递参数,可以连同 ()
小括号一起省略可变参数
动态内存管理
在 C/C++中内存可分为两种类型:
- 栈内存:(stack)一般用来存储局部变量和函数的参数,它的分配和释放由编译器自动完成;
- 堆内存:(heap)堆内存比栈内存要大得多,但是它的分配和释放需要手动完成。是所有程序共同拥有的自由内存。
C 标准库 <cstdlib>
为内存的分配和管理提供了四个函数
Function | Description |
---|---|
void* calloc(int num, int size) |
分配一个 num 个元素的数组,每个元素的大小为 size 字节,返回指针 |
void free(void *address) |
释放 address 所指向的内存块 |
void* malloc(int num) |
配一个 num 字节的内存块,返回指针 |
void* realloc(void *address, int newsize) |
重新分配内存,把内存扩展到 newsize,返回指针 |
注意:void* 类型表示未确定类型的指针。C/C++ 规定 void* 类型可以通过类型转换强制转换为任何其它类型的指针。
在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收。
int* ptr = (int*)malloc(sizeof(int) * 10); |
C++ 又新增了两个关键字 new
和delete
来更加简单的分配内存。new
操作符会根据后面的数据类型来推断所需空间的大小。delete
用来释放内存。
int* ptr = new int; |
如果希望使用一组连续的内存,可以使用 new
来分配,使用 delete[]
来释放内存。
int* ptr = new int[10]; |
在实际开发中,new
和 delete
往往成对出现,以保证及时删除不再使用的对象,防止无用内存堆积。
面向对象
C++ 是一种功能强大的高级编程语言,它在 C 语言的基础上增加了面向对象编程的特性。
类与对象
类(class)是一种用户定义的数据类型,它包含自己的属性和方法,可以通过创建该类的实例来访问和使用它们。
|
上例中 Person
是类名称,类名的首字母一般大写。{ }
内部是类所包含的属性和方法,它们统称为类的成员(Member)。定义类外面的方法必须使用域解析符 ::
指明当前函数所属的类。
创建对象以后,可以使用点号.
来访问属性和方法。
int main() |
在 C++ 中,指向类的指针与指向结构的指针类似
int main() |
封装
C++ 通过访问修饰符 public, protected, private
来控制类属性和方法的访问权限。
public
:类的所有成员都是公开的,可以在任何地方访问。protected
:类成员可以被类及其子类访问。private
:类成员只能在类的内部访问,默认修饰符。
|
构造函数
构造函数(Constructor)是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,并且不需要 void
声明。一个类可以有多个重载的构造函数。如果用户没有定义,编译器会自动生成一个默认的构造函数。
|
析构函数
析构函数(Destructor)是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~
)作为前缀,它不会返回任何值,也不能带有任何参数。如果用户没有定义,编译器会自动生成一个默认的析构函数。
C++ 中的 new
和 delete
分别用来分配和释放内存,它们与 C 语言中 malloc()
、free()
最大的一个不同之处在于:用 new
分配内存时会调用构造函数,用 delete
释放内存时会调用析构函数。
|
this 指针
this
是 C++ 类中的隐式形参,它是指向当前对象的指针,通过它可以访问当前对象的所有成员,包括 public, protected, private
权限的。
class Person { |
注意,this
是一个指针,要用 ->
来访问属性和方法。本例中类方法的参数和属性重名,只能通过 this
区分。
友元函数
友元函数不是类的成员函数,通过在类内使用关键字 friend
来声明,可以访问当前类中的所有成员,包括 public, protected, private
权限的。
|
继承
继承(Inheritance)允许一个派生类继承基类的属性和方法。
在 C++中,有 3 种继承模式:public, protected, private
,用来指明基类成员在派生类中的最高访问权限。
|
C++ 中基类的构造函数不能被继承,编译器会自动按照继承顺序调用基类的默认构造函数,这意味着将首先调用基类构造函数,然后调用派生类构造函数。基类的参数化构造函数则必须在派生类构造函数的初始化列表中显式调用。
class Parent { |
Output
Inside base class |
函数覆盖
在 C++中,派生类会覆盖基类中相同类型的属性和方法。
// C++ program for function overriding |
运算符重载
运算符重载(Operator Overloading)允许改变运算符的行为,以适应用户定义的类型。operator
关键字专门用于定义重载运算符的函数。虽然运算符重载所实现的功能完全可以用函数替代,但运算符重载使得程序的书写更加人性化,易于阅读。
|
虚函数
虚函数允许我们使用基类的指针或引用调用任何派生类的方法,甚至可以在不知道派生类对象类型的情况下调用。虚拟函数是使用关键字 virtual
在基类中声明的方法,并在派生类中覆盖。
// C++ program for virtual function overriding |
Output
print derived class |
类模板
正如我们定义函数模板一样,我们也可以定义类模板。类模板在类定义独立于数据类型的内容时非常有用。
// C++ Program to implement |
类与头文件
我们知道可以将函数声明放在头文件中。然后可以将这些函数声明#include
到多个代码文件(甚至多个项目)中。类也不例外。类定义可以放在头文件中,然后#include
在要使用类类型的任何其他文件中。
与只需要使用前向声明的函数不同,编译器通常需要查看类的完整定义,才能使用类型。这是因为编译器需要理解如何声明成员,以确保正确使用它们,并且它需要能够计算该类型的对象的大小,以便实例化它们。因此,头文件通常包含类的完整定义,而不仅仅是类的前向声明。
通常,类在与类同名的头文件中定义,在类之外定义的任何成员函数都放在与类相同名称的.cpp
文件中。
这里是我们的 Date 类,分为 .cpp
和.h
文件:
// Date.h |
// Date.cpp |
现在,任何其他想要使用 Date 类的头文件或代码文件都可以简单地#include “Date.h” 。请注意,date.cpp 还需要编译到任何使用 date.h 的项目中,以便链接器可以将对成员函数的调用连接到其定义。
最佳实践:优先将类定义放在与类同名的头文件中。琐碎的成员函数(例如访问函数、具有空函数体的构造函数等)可以在声明文件中定义。首选在与类同名的源文件中定义非平凡的成员函数。
异常处理
C++ 异常处理涉及到三个关键字:try、catch、throw。
try { |
当异常发生时,会立即终止当前函数并开始查找匹配的 catch 块来处理引发的异常。C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 的子类,称为标准异常(Standard Exception)。
|
Output
Exception Division by zero not allowed! |
文件和流
cstdio
C 头文件 <cstdio>
包含了一个专门用来处理文件的数据类型 FILE
。每当程序执行文件打开操作,其返回值是一个 FILE
类型的指针,所有关于文件的操作都要通过此指针来进行。
FILE* fopen (filename, mode) |
// C program to Open a File, |
fstream
在 C++ 中,<fstream>
是标准库中用于文件输入输出操作的类。它提供了一种方便的方式来读写文件。
// C++ program to Open a File, |
上例中 getline(fio, line)
函数用于从文件流中读取整行字符串。EOF (End of File) 是在 iostream
类中定义的一个整型常量,值为 -1。
打开文件的常见的模式有:
std::ios::in
:以输入模式打开文件。std::ios::out
:以输出模式打开文件。std::ios::app
:以追加模式打开文件。std::ios::ate
:打开文件并定位到文件末尾。std::ios::trunc
:打开文件并截断文件,即清空文件内容。
预处理指令
预处理器是在实际编译开始之前处理源代码的程序。所有预处理的指令,必须独占一行,并以 #
开始,末尾不加分号。
宏定义
#define
预处理指令用于创建符号常量,该符号常量通常称为宏。关键字 #define
和 #undef
用于在 C 中创建和删除宏。
我们还可以将参数传递给宏。这些宏的工作方式类似于函数。
参数加括号是因为宏定义只是简单的替换。
ANSI C 定义了许多宏,它们的名字的前后有两个下划线作为标识,例如:
__LINE__
代表源代码文件中的当前行__FILE__
代表文件的名字__DATE__
表示编译日期,格式为mmm dd yyyy
__TIME__
表示编译时间,格式为hh:mm:ss
头文件
一个项目往往包含多个源码文件,通常的做法是创建一个 头文件,然后使用预处理器指令 #include
加载进入当前文件。头文件用于放置对应源文件里面函数声明等内容,不包括函数定义和实现。
有两种形式:
尖括号 <>
是用来引入标准库中的头文件,双引号是用来引入用户自定义的头文件。
C++头文件不是以 .h
做扩展名,C 语言中的标准头文件如 math.h, stdio.h
在 C++中被命名为 cmath, cstdio
。
例如,我们想在主程序中引入一个函数 add
// add.cpp |
需要创建一个和源文件同名的头文件 add.h
,里边只放入函数声明
// add.h |
然后就可以在主文件引入使用了
// main.cpp |
编译多个文件组成的程序时,需要在命令行中列出它们:
g++ -o main main.cpp add.cpp |
如果编译的文件分别位于不同的目录下,编译时可以通过 -I
选项来指明头文件搜索路径
g++ -o main -I/source/includes main.cpp |
条件编译
头文件里面还可以加载其他头文件,因此有可能产生重复加载,这将产生错误。为了防止这种情况,标准的做法是每个头文件都包含头文件包含。预处理指令包括:#if, #ifdef, #ifndef, else, #elif, #endif
|
这时,当头文件被包含时,预处理器会检查 HEADER_FILE_H 是否已经被定义过。如果该头文件之前已经被包含了,那么预处理器会跳过文件的整个内容。
所有的头文件都应该有头文件保护。但根据惯例,它被设置为头文件的完整文件名,以大写字母键入,使用下划线表示空格或标点。标准库头文件也使用头文件保护。
现代编译器使用更简单的 #pragma
请求编译器保护头文件
|
目前对 #pragma once
的支持是相当普遍的,由于不是由 C++标准定义的,因此一些编译器可能不会实现它。
模块管理
模块
从 C++20 开始,C++ 引入了模块(Modules),并在 C++23 中进一步完善了对标准库模块的支持。模块提供了一种更高效、更安全的方式来导入标准库。模块只编译一次,后续导入时直接使用编译好的二进制接口。
import std; |
或者导入标准库的特定部分:
import std.core; |
目前,主流编译器对 C++ 模块的支持正在逐步完善。以下是一些编译器启用 C++23 标准和模块支持的方法:
- GCC:
g++ -std=c++23 -fmodules-ts -o program main.cpp
- Clang:
clang++ -std=c++23 -fmodules -o program main.cpp
- MSVC(Visual Studio):
cl /std:c++23 /experimental:module /EHsc /Fe:program main.cpp
static
正常情况下,当前文件内部的全局变量,可以被其他文件使用。有时候,不希望发生这种情况,而是希望某个变量只局限在当前文件内部使用,不要被其他文件引用。这时可以在声明变量的时候,使用 static
关键字,使得该变量变成当前文件的私有变量。
// Variable with internal linkage |