C++ 基础速成篇
Published in:2024-12-28 |

C++ 和 C 语言的对比

  • C++ 是 C语言的超集:C++ 是 C语言的扩展。几乎所有合法的 C代码在 C++ 中都能正常工作。C++ 在 C语言的基础上增加了面向对象(OOP)特性(类、继承、多态等)、模板、异常处理、STL(标准模板库)等现代编程功能。简单来说,C++包含了 C 语言的所有功能,并在此基础上进行了扩展。

学习 C++ 前是否需要先学习 C语言?
没有学习 C语言也是可以直接学习C++ 的。但是如果有了 C语言基础,可以更好地学习 C++ 相关知识。

  • C语言的设计非常接近硬件,它没有过多的抽象,非常适合学习操作系统、嵌入式编程以及深入了解计算机内部工作原理。
  • 学习 C语言有助于理解一些重要概念,如内存管理、指针、数据结构等,这些知识对C++ 编程同样适用。
  • 如果读者已经熟悉C语言的低级特性(例如指针和内存管理),那么在学习C++时,会更容易理解 C++ 中的复杂特性(如对象的内存管理)。

阅读本文前,最好是拥有C语言的基础,以实现 C++ 基础速成的目的。

学习 C++ 后可以做什么?

  • 系统编程:
    • 操作系统开发:C++ 由于其接近硬件的特性,广泛用于操作系统开发。可以使用 C++ 开发高效的操作系统或操作系统内核的模块。
    • 驱动程序开发:硬件驱动程序通常用 C 或 C++ 编写,它们直接与操作系统和硬件交互。
    • 嵌入式开发:C++ 常用于嵌入式系统(如微控制器、物联网设备等),这类系统要求高效和低资源消耗,C++ 的控制性和高性能使其成为理想选择。
  • 游戏开发:
    • 游戏引擎开发:C++ 是许多知名游戏引擎(如 Unreal Engine)的核心语言。学习 C++ 后,可以参与游戏引擎的开发或在现有引擎上进行定制化开发。
    • 游戏开发:C++ 是大多数 AAA 级游戏(如《上古卷轴》系列、《巫师》系列等)的主要开发语言。可以使用 C++ 开发高性能、资源密集型的游戏。
    • 图形编程与渲染:C++ 在图形编程中被广泛使用,尤其是在与 OpenGL、DirectX、Vulkan 等图形 API 进行交互时。
  • Web 后端开发、人工智能与机器学习等等。

C++ 编译器

  • 在 Linux 系统(如 Ubuntu)上,C++ 的代码可以使用 g++ 编译器进行编译。(C语言代码使用 gcc 编译)。
  • 在 Windows 系统上,可以使用 DevCpp 或者 Visual Studio 来编译 C++ 代码。DevCpp 是比较轻量的 C/C++ 编译器,而且是完全免费的,适合用于学习。而 Visual Studio 占内存比较大,虽然功能比较丰富,但是安装时间比较久,学习使用周期长。对于初学者而言,单纯想要快速掌握 C/C++ 语法,使用 DevCpp 编译器即可。如果以后有更大的项目需求,转而学习使用 Visual Studio 即可。(Visual Studio 使用的 C/C++ 编译器是 CL.exe ,链接器是 LINK.exe ,资源编译器是 RE.exe)。

不同的 CPU 架构(如 x86ARM 等)对应着不同的 CPU 指令集(如 CISC 复杂指令集计算、RISC 精简指令集计算等)。

  • x86 架构的 CPU 主要用于 桌面计算机、服务器 和一些嵌入式系统(例如工业控制系统、一些高性能计算系统)。
  • ARM 架构的 CPU 主要应用于 移动设备(如智能手机、平板电脑)以及 部分桌面计算机嵌入式系统

不同的指令集对应着不同的 汇编语言

  • C/C++ 编译器在编译源代码时,首先将源代码转换为 目标指令集 对应的 汇编语言
  • 然后,编译器使用对应平台的 汇编器 将生成的汇编语言代码转换为特定 CPU 架构的 二进制机器码(通常为 目标文件,即 .o.obj 文件)。
  • 最后,链接器将多个目标文件和库文件连接成一个 可执行程序

这些可执行程序只能在其对应的 CPU 架构 上运行,因为每种架构的机器码格式和指令集是特定于该架构的。

不同操作系统实现了自己的 C/C++ 编译器工具链,这些工具链用于处理源代码到可执行文件的整个编译过程。

Linux 系统:GCC / G++,GCC/G++ 工具链内置了链接器(通常是 ld),负责将多个目标文件和库文件链接成最终的可执行文件。

Windows 系统

  • CL.exe:在 Windows 平台上,最常用的 C/C++ 编译器是 **Microsoft Visual C++**(MSVC)中的 CL.exe。它将 C/C++ 源代码编译成机器代码。
  • LINK.exe:这是 Windows 中的链接器,负责将 .obj 目标文件和库文件链接成最终的可执行文件。
  • RE.exe:资源编译器,用于将资源文件(如图标、对话框、字符串等)编译成 .res 文件,并将它们与目标文件一起链接成可执行程序。

macOS 系统

  • Clang:在 macOS 上,编译 C/C++ 代码时通常使用 Clang 编译器。Clang 是一个现代化的编译器,兼容 GCC,同时也提供了更好的诊断信息。
  • 链接器:macOS 使用 ld 作为链接器,将目标文件和库文件链接成最终的可执行程序。

早期的 C语言编译器本身是由汇编语言开发的,因为在C语言编译器诞生之前不可能使用 C语言来开发C语言的编译器。C 语言最初由 Dennis Ritchie 在 1972 年左右开发,它的第一个编译器(称为 B 编译器)是基于 B 语言 的。B 语言是由 Ken Thompson 在 1969 年开发的,它本身也是由 汇编语言 实现的。因此,最早的 C 编译器并没有使用 C 本身,而是用 汇编语言 来实现。

随着 C 语言的普及和它自身的成熟,C 语言的编译器开始逐步用 C 语言 本身实现。这是由于 C 本身是一种高效且便于移植的语言,适合开发编译器。历史上最具里程碑意义的例子是 第一代 C 编译器,它被用来编译自己。

cc 编译器(通常称为 C 编译器)

  • 这个编译器是用 C 语言编写的,标志着 C 语言已经能够用于实现自己的编译器。
  • 这个过程被称为 自举(bootstrapping):使用编译器编译自身。

1973 年的 Unix 系统

  • 在 Unix 操作系统的早期版本中,C 编译器开始被用来编译 Unix 系统本身,Unix 操作系统也逐步从汇编语言移植到了 C 语言。
  • 这个 C 编译器就是在 Unix 操作系统上开发的。

C++ 语言是在 1983 年由 Bjarne Stroustrup 开发的,它最初是在 C 语言 的基础上扩展而来。最初的 C++ 编译器(例如 Cfront)也是用 C 语言开发的。

  • Cfront:
    • 第一个 C++ 编译器是 Cfront,由 Stroustrup 开发,最初是用 C 语言 开发的。
    • Cfront 将 C++ 源代码翻译为 C 代码,然后再用 C 编译器(通常是 cc)编译生成目标代码。
    • 这个过程称为 源到源编译(source-to-source compilation)

现代的一些高级编程语言如 Java / Python 的内核都是由 C 或 C++ 来实现的。

JDK 的内核(特别是 JVM 和一些底层功能)是用 C 语言实现的。尽管 Java 本身是一种高级编程语言,其核心部分(如 JVM、垃圾回收器、内存管理、I/O 系统 等)是由 C 或 C++ 编写的,主要原因是 C 语言具有接近硬件的性能,能够高效地处理底层操作。

JVM 是 Java 程序能够跨平台运行的关键。它负责将 Java 字节码 转换为特定平台的机器代码并执行。这也就是为什么相同的 Java 代码可以既可以在 Windows 上运行也可以在 Linux 上运行的原因。

Python 也是同样的道理,Python 代码的运行依赖于 Python 解释器,而Python 解释器在 Windows 上和 Linux 上均有实现。Python 的核心实现(CPython) 是用 C 语言 编写的,负责执行 Python 字节码并管理内存。CPython 是目前最常用的 Python 实现,它使得 Python 代码可以在不同平台(如 Windows、Linux、macOS 等)上运行。




标准输入输出流

C++ 打印 Hello World

1
2
3
4
5
6
//learn.cpp
#include <iostream>
int main(void) {
std::cout << "Hello World" << std::endl;
return 0;
}

Linux terminal 下使用 G++ 编译器编译 learn.cpp 源代码的命令:g++ -o hello learn.cpp ,将 learn.cpp 里面的源代码编译成可执行文件 hello,运行使用命令 ./hello./ 代表当前目录下,hello 代表可执行文件的名字。

1
2
3
4
5
jackeysong@jackey-song:~/Desktop/cpp$ ls
learn.cpp
jackeysong@jackey-song:~/Desktop/cpp$ g++ -o hello learn.cpp
jackeysong@jackey-song:~/Desktop/cpp$ ./hello
Hello World

想要使用标准输入输出流,首先要 #include <iostream>
<iostream> 是标准 C++ 库的一部分,定义了输入输出流所需的类和对象,例如:

  • **std::cin**:标准输入流。
  • **std::cout**:标准输出流。
  • **std::cerr**:标准错误流。
    1
    std::cerr << "Error occurred!" << std::endl;
  • **std::clog**:标准日志流。
    1
    std::clog << "Log message" << std::endl;

这些对象和相关功能都声明在 <iostream> 中。

主函数的声明和 C语言基本一样:

1
2
3
int main(void) {
return 0;
}

带参数的主函数:

1
2
3
int main(int argc, char* argv[]) {
return 0;
}

接受宽字符参数的形式(C++ 特有):

1
2
3
int main(int argc, wchar_t* argv[]) {    //用于处理宽字符命令行参数。
return 0;
}

std:: 是 C++ 中 标准命名空间(namespace) 的缩写,用于表示标准库中定义的所有类、函数和对象的作用域。

命名空间的作用

命名冲突的解决

  • 在大型程序中,不同模块可能会定义相同的类或函数名。为了避免名称冲突,C++ 使用命名空间将相关的名字分组。
  • std 是 C++ 标准库的命名空间,所有标准库中的内容都位于 std 命名空间中。
  • 例如,coutcinvector 等标准库对象和类都属于 std

使用规则

  • 使用标准库内容时,需要在前面加上 std:: 以明确作用域,例如 std::coutstd::cin

输入/输出运算符 >> 和 <<

输出运算符 <<:在标准输入输出流中,<< 用于将数据输出到流中(通常是 std::cout)。

**std::cout**:

  • std::cout 是 C++ 标准库中的 标准输出流对象,用于将数据输出到终端(通常是控制台)。
  • cout 定义在 std 命名空间中,所以需要使用 std:: 前缀。

<< 输出运算符用于将右侧的数据插入到左侧的输出流中。

std::cout << "Hello World" << std::endl; 将字符串 "Hello World" 发送到 std::cout,从而打印到屏幕。std::endl 是一个操纵符(manipulator),用于向输出流插入一个换行符(等价于 \n),并刷新输出流缓冲区,确保所有数据都立即输出到终端。在某些情况下,C++ 的输出是缓冲的(即数据可能会暂时存储在内存中,而不是立即输出到屏幕)。std::endl 确保数据立刻被输出,这在实时日志或调试时非常重要。

using

using 是 C++ 中的一个关键字,具有多种用途,主要用于简化命名、创建类型别名以及引入命名空间中的成员。

using 关键字可以用来引入命名空间中的特定成员或整个命名空间,以简化代码书写。

  • 引入命令空间中的特定成员:using namespace_name::member_name;
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

//引入命名空间中的特定成员
using std::cout;
using std::endl;

int main() {
cout << "Hello, World!" << endl; // 无需写 std::
cout << "Jackey Song with C++\n";
cout << "The result of 3 times 4 is :" << 3 * 4 << '\n';
return 0;
}
1
2
3
4
5
jackeysong@jackey-song:~/Desktop/cpp$ g++ -o hello learn.cpp
jackeysong@jackey-song:~/Desktop/cpp$ ./hello
Hello, World!
Jackey Song with C++
The result of 3 times 4 is :12
  • 引入整个命令空间:using namespace namespace_name;
  • 引入整个命名空间不推荐在大型项目中使用,因为它可能引发命名冲突,特别是当多个命名空间有相同的成员名称时。
1
2
3
4
5
6
7
8
9
10
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
for (int i = 0; i < argc; i++) {
cout << "第" << i + 1 << "/" << argc << "个参数: " << argv[i] << endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
jackeysong@jackey-song:~/Desktop/cpp$ g++ -o hello learn.cpp
jackeysong@jackey-song:~/Desktop/cpp$ ./hello Talk is cheap, show me the code.
第1/8个参数: ./hello
第2/8个参数: Talk
第3/8个参数: is
第4/8个参数: cheap,
第5/8个参数: show
第6/8个参数: me
第7/8个参数: the
第8/8个参数: code.

using 可以用来为已有类型创建别名,从 C++11 开始是推荐的写法(取代 typedef)。

using new_type_name = existing_type_name;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>

// 创建别名
using IntVector = std::vector<int>;
//typedef std::vector<int> IntVector;

int main() {
IntVector numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
std::cout << num << " ";
}
return 0;
}

输入运算符 >>:在标准输入输出流中,>> 用于从流中读取数据(通常是 std::cin)。

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
int main() {
int x;
cout << "Enter a number: ";
cin >> x;
cout << "You entered: " << x << endl;
return 0;
}
1
2
3
4
jackeysong@jackey-song:~/Desktop/cpp$ g++ -o hello learn.cpp
jackeysong@jackey-song:~/Desktop/cpp$ ./hello
Enter a number: 1024
You entered: 1024

根据代码的上下文,<< / >> 也可作为位运算符中的左移运算符和右移运算符。

运算符 上下文 功能
<< 位运算 左移,补 0,效果类似乘以 2^n
>> 位运算 右移,左侧补 0 或符号位,效果类似除以 2^n
<< 输出流 将右侧数据输出到左侧流(如 std::cout
>> 输入流 从左侧流读取数据到右侧变量(如 std::cin

C++ 中的数据类型

C 和 C++ 共有的基本数据类型

数据类型 大小(通常) 描述
char 1 字节 表示单个字符,通常是 8 位。
int 4 字节 表示整数类型。大小可能依赖系统架构。
float 4 字节 表示单精度浮点数。
double 8 字节 表示双精度浮点数。
short 2 字节 短整数,范围小于 int
long 4 或 8 字节 长整数,范围大于或等于 int
long long 通常为 8 字节 表示更大的整数。
_Bool 1 字节(C99 引入) 表示布尔值,truefalse
1
2
3
4
5
6
7
8
9
10
11
12
//learn.c
#include <stdio.h>
#include <stdbool.h>
int main() {
_Bool a = true;
if (a) {
printf("Yes\n");
} else {
printf("No\n");
}
return 0;
}
1
2
3
//compile: gcc -o learn learn.cpp
//run: ./learn
Yes

C++ 中特有的数据类型

布尔类型

1
2
3
4
5
6
7
#include <iostream>
int main() {
bool isTrue = true;
if (isTrue) std::cout << "Yes" << std::endl;
else std::cout << "No" << std::endl;
return 0;
}
1
Yes

宽字符类型

  • **wchar_t**:表示宽字符类型,用于多字节字符。

  • char16_tchar32_t

    (C++11 引入):分别表示 UTF-16 和 UTF-32 编码字符。

1
2
char16_t ch16 = u'好';
char32_t ch32 = U'😊';

自动类型推导

  • auto

    (C++11 引入):让编译器自动推导变量类型。

    • 示例:

      1
      2
      auto x = 10;      // x 是 int
      auto y = 3.14; // y 是 double
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main() {
auto a = 10; // 推导为 int 类型
auto b = 3.14; // 推导为 double 类型

cout << typeid(a).name() << " " << typeid(b).name() << endl;

return 0;
}
1
2
3
4
//run:
i d
// i 代表 int
// d 代表 double

字符串类型

C++ 中除了可以使用 C语言中字符数组形式的字符串外,也可以使用 C++ 标准库中的 std::string 类。

1
2
3
4
5
6
#include <iostream>
int main() {
char str[] = "Hello, World"; // 自动添加 '\0' 结尾。C风格的字符串数组
std::cout << str << std::endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string> // 引入 string 头文件
using namespace std;
int main() {
std::string str = "Hello, World"; // 使用 std::string 类型初始化
cout << str << endl;
cout << "Length:" << str.length() << endl;
std::cout << "Length:" << str.size() << std::endl;

string str1 = "Jackey ";
string str2 = "Song";
string result = str1 + str2;
cout << "result of str1 + str2: " << result << endl;
return 0;
}

Windows DevCpp 编译运行:

在这里插入图片描述

1
2
3
std::string s = "Hello";
std::cout << s[0] << endl; // 使用下标访问第一个字母 'H'
std::cout << s.at(0) << endl; // 使用 at() 方法访问第一个字符 'H',at() 会在访问越界时抛出 std::out_of_range 异常。
1
2
3
4
// 字符串的拼接除了可以使用 + 号,也可以使用 append() 方法
std::string s1 = "Hello";
std::string s2 = "World";
s1.append(s2);

std::string 字符串比较可以使用 ==、!=、<、> 等操作符。

1
2
3
4
5
6
string s1 = "Hello";
string s2 = "hello";
if (s1 == s2) //判断字符串是否相等,C语言使用 !strcmp(s1, s2)
cout << "s1 equals to s2." << endl;
else
cout << "s1 not equals to s2." << endl;

<> 比较字符串的字典顺序(ASCII 码值的比较)。


查找子字符串:find() ,返回子字符串首次出现的位置,没有则返回 std::string::npos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
using namespace std;
void findSubstring(string str, string target) {
size_t pos = str.find(target);
if (pos != string::npos) {
cout << "'" << target << "'" << " found at position " << pos << endl;
} else {
cout << "'" << target << "'" << " not found" << endl;
}
}
int main() {
string s = "Hello everyone. My name is Jackey Song. Nice to meet you.";
findSubstring(s, "Jackey");
findSubstring(s, "Alice");
return 0;
}
1
2
'Jackey' found at position 27
'Alice' not found

提取子字符串:substr() ,提取从给定位置开始的指定长度的子字符串。

1
2
string s = "Hello everyone. My name is Jackey Song. Nice to meet you.";
cout << s.substr(27, 6) << endl; // 输出 Jackey

替换子字符串:replace() ,用新字符串替换原字符串中的一部分。

1
2
3
string s = "Hello, World!";
s.replace(7, 5, "C++"); // 从位置 7 开始,替换 5 个字符为 "C++"
cout << s << endl; // 输出 "Hello, C++!"

删除部分子字符串:erase() ,用于删除指定位置和长度的字符。

1
2
3
std::string s = "Hello, World!";
s.erase(5, 7); // 从位置 5 开始,删除 7 个字符
std::cout << s << std::endl; // 输出 "Hello!"

插入子字符串:insert() ,在指定位置插入给定的字符串。

1
2
3
std::string s = "Hello!";
s.insert(5, " World"); // 在位置 5 插入 " World"
std::cout << s << std::endl; // 输出 "Hello World!"

大小写转换:std::transform()::toupper / ::tolower 来实现大小写转换。

1
2
3
4
5
6
7
#include <algorithm>     // 包含了 std::transform() 
#include <cctype>

std::string s = "Hello, World!";
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
//字符串开始到结尾进行迭代,输出迭代器为 s.begin() 意味着覆盖原始字符串。::toupper 变为大写。
std::cout << s << std::endl; // 输出 "HELLO, WORLD!"

std::transform() 是 C++ 标准库中的一个算法,通常用来对容器中的元素进行转换。它属于 <algorithm> 头文件,可以用来对字符串、数组或其他容器的元素进行逐个转换,例如将字符串中的字符转换为大写或小写。

**::toupper::tolower**:用于转换每个字符的操作。std::toupperstd::tolower 是定义在 <cctype> 中的函数,用于分别将字符转换为大写和小写。

#include <cctype> 是一个 C++ 标准库头文件,提供了一组用于处理字符的 C 风格函数,这些函数主要用于检查和转换单个字符的类型和属性,比如判断字符是否是数字、字母,或者将字符转换为大小写。这些函数是从 C 语言继承过来的,并在 C++ 中可以直接使用。

<cctype> 提供了一系列操作 单个字符 的函数,主要包括:

  1. 检查字符的属性

    这些函数返回一个布尔值,表示字符是否具有某种特定属性。

    函数 描述
    isalnum(c) 检查字符是否为字母或数字(字母数字字符)。
    isalpha(c) 检查字符是否为字母(仅字母字符)。
    isdigit(c) 检查字符是否为数字(仅数字字符)。
    islower(c) 检查字符是否为小写字母。
    isupper(c) 检查字符是否为大写字母。
    isspace(c) 检查字符是否为空白字符(如空格、换行、制表符)。
    ispunct(c) 检查字符是否为标点符号。
    isxdigit(c) 检查字符是否为十六进制数字(0-9, A-F, a-f)。
  2. 转换字符的大小写

    这些函数用于将字符转换为大写或小写字母。

    函数 描述
    tolower(c) 将字符转换为小写字母。
    toupper(c) 将字符转换为大写字母。

字符串转换为数字:std::stoi()std::stol()std::stod() 等方法将字符串转换为数字。(某些编译器可能不支持这些函数。)

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>
int main() {
std::string s_num = "123";
std::string s_pi = "3.1415926";
int num = std::stoi(s_num);
double pi = std::stod(s_pi);
std::cout << num * 10 << std::endl;
std::cout << pi * 100 << std::endl;
return 0;
}
1
2
3
4
jackeysong@jackey-song:~/Desktop/cpp$ g++ -o hello learn.cpp            #编译
jackeysong@jackey-song:~/Desktop/cpp$ ./hello #运行
1230
314.159

string.empty() 检查字符串是否为空。

1
2
3
string a = "";
if (a.empty())
cout << "String a is empty." << endl; //输出: String a is empty.

字符串与 C风格字符串之间的转换:使用 c_str() 方法将 std::string 类型的字符串转换为 C语言风格的字符串 const char*

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string> // C++ 字符串类
#include <string.h> //包含了 C语言 strcmp() 函数的声明。
using namespace std;
int main() {
string s1 = "hello";
string s2 = "hello";
if (s1 == s2)
cout << "C++: s1 equals to s2." << endl;
if (!strcmp(s1.c_str(), s2.c_str()))
printf("C: s1 equals to s2.\n");
return 0;
}
1
2
3
4
jackeysong@jackey-song:~/Desktop/cpp$ g++ -o hello learn.cpp
jackeysong@jackey-song:~/Desktop/cpp$ ./hello
C++: s1 equals to s2.
C: s1 equals to s2.

面向对象编程思想

面向对象编程(OOP,Object-Oriented Programming)是一种编程范式,它将程序设计为由对象和类组成的系统。面向对象的主要思想是通过封装、继承和多态等机制来组织代码,使程序更易于理解、维护和扩展。

对象 - Object

  • 定义:对象是数据和操作数据的函数(称为方法)的集合。对象通常是类的实例。
  • 属性:对象的状态由属性(或字段)描述,这些属性通常是类定义的成员变量。
  • 行为:对象的行为由类中的方法定义,方法是与对象相关联的操作。

类 - Class

  • 定义:类是创建对象的蓝图或模板。类定义了对象的属性(数据)和行为(方法)。通过类,程序员可以创建多个相似类型的对象。
  • 构造函数和析构函数:构造函数用于初始化对象的状态,析构函数在对象销毁时执行清理工作。

封装 - Encapsulation

  • 定义:封装是将数据(属性)和对数据的操作(方法)打包到一起,并隐藏实现细节,只暴露必要的接口。
  • 访问控制:通过 publicprivateprotected 关键字来控制类成员的访问权限。这样,外部代码不能直接访问对象的内部数据,确保数据的安全性和一致性。

继承 - Inheritance

  • 定义:继承是通过创建新类来复用、扩展或修改现有类的功能。子类(派生类)继承父类(基类)的属性和方法,可以新增或修改父类的行为。
  • 好处:继承提高了代码的复用性,减少了冗余代码。
  • 访问控制:通过 publicprotectedprivate 继承,控制子类如何继承父类的成员。

多态 - Polymorphism

  • 定义:多态是指相同的接口可以表现出不同的行为。多态可以通过方法重载(同名不同参数的方法)和方法覆盖(继承类中重写父类的方法)来实现。
  • 虚函数:通过虚函数实现动态绑定,从而支持运行时多态。虚函数在基类中声明,并在派生类中重写。
  • 好处:多态性使得代码更加灵活,能够处理不同类型的对象。

抽象 - Abstraction

  • 定义:抽象是通过隐藏具体实现来简化复杂系统的过程。抽象化的目的是让程序员关注接口,而不是实现细节。
  • 抽象类:抽象类是包含至少一个纯虚函数(没有实现的虚函数)类,不能实例化,但可以作为基类供派生类继承。



C++ 类 class

C++ 中类(class)是面向对象编程的核心,类是创建对象的蓝图,定义了对象的属性(数据成员)和行为(成员函数)。

定义类的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ClassName {
// 访问权限说明符(可选):public, private, protected
// 数据成员(属性)
// 成员函数(方法)
public: // 公有成员:对外可访问
int publicAttribute;
void publicMethod();

private: // 私有成员:仅类内部可访问
int privateAttribute;
void privateMethod();

protected: // 受保护成员:仅类及其子类可访问
int protectedAttribute;
}; //末尾分好不可少

类的成员

  • 数据成员
    • 数据成员表示类的属性或状态。
    • 数据成员可以是基本数据类型、指针、引用、数组或其他类的对象。
  • 成员函数
    • 成员函数表示类的行为或操作。
    • 成员函数通常用于操作或访问数据成员。
    • 成员函数可以定义在类内部或类外部。
  • 静态成员
    • 静态成员变量:所有对象共享,生命周期贯穿程序运行期。
    • 静态成员函数:与类关联,可通过类名直接调用。

类的访问控制

C++ 中类的成员有 3 种访问控制方式:

  1. **public**(公有):对类外部和类内部都可访问。
  2. **private**(私有):只能在类内部访问,默认的访问权限。
  3. **protected**(受保护):只能在类内部和子类中访问。
1
2
3
4
5
6
7
8
class Example {
public:
int publicVar; // 公有成员
private:
int privateVar; // 私有成员
protected:
int protectedVar; // 受保护成员
};

类的特性

构造函数 - Constructor

  • 定义:

    • 构造函数是一个特殊的成员函数,在创建对象时自动调用,用于初始化对象的成员变量。
    • 构造函数的名称与类名相同,没有返回值。
  • 特点:

    • 自动调用:当对象被创建时,构造函数自动执行。
    • 支持重载:可以定义多个构造函数(通过不同的参数列表)。
    • 默认构造函数:无参构造函数,如果程序员未定义,编译器会自动生成一个默认的无参构造函数。
    • 可以使用初始化列表来高效初始化成员变量。

析构函数 - Destructor

  • 定义:
    • 析构函数是一个特殊的成员函数,在对象生命周期结束时自动调用,用于释放资源、清理对象。
    • 析构函数的名称是类名前加一个波浪号(~),没有返回值,也不接受参数。
  • 特点:
    • 自动调用:当对象超出作用域或被显式销毁时(例如通过 delete),析构函数自动执行。
    • 每个类只能有一个析构函数(不能重载)。
    • 通常用于释放动态分配的资源(如内存、文件句柄等)。

拷贝构造函数 - Copy Constructor

  • 定义:
    • 拷贝构造函数是用来创建一个对象,并用同类型的另一个对象对它进行初始化。
    • 默认拷贝构造函数是由编译器生成的,执行 逐成员拷贝(浅拷贝)。
  • 特点:
    • 形式为 ClassName(const ClassName& obj)
    • 当需要 深拷贝 时,程序员需要自定义拷贝构造函数。
    • 在以下情况下会调用拷贝构造函数:
      • 用一个对象初始化另一个对象,例如 ClassName obj2 = obj1;
      • 对象按值传递给函数。
      • 函数按值返回对象。
  • 浅拷贝 VS 深拷贝:
    • 浅拷贝:只复制成员变量的值,对于指针成员仅复制地址,可能导致悬空指针问题。
    • 深拷贝:复制指针指向的内容,为指针成员分配独立的内存。
1
2
3
4
5
6
7
// 初始化一个对象为另一个对象的副本。
class MyClass {
public:
MyClass(const MyClass& obj) { // 引用 &
cout << "Copy constructor called" << endl;
}
};

常量成员函数

常量成员函数(const member function) 是一种在 C++ 中声明为不能修改对象状态(即不能修改对象的非静态数据成员)的成员函数。

常量成员函数通过在函数声明和定义的尾部加上 const 关键字来标识:

1
ReturnType FunctionName(ParameterList) const;
  • 位置:const 关键字位于参数列表的右侧、函数体的左侧。
  • 作用:告诉编译器,该函数不会对所属对象的成员变量进行修改。

静态成员

与类本身相关,可通过类名直接调用。

类的定义与实现分离

头文件与实现文件

  • 类的声明通常放在头文件(.h 文件)。
  • 类的实现放在源文件(.cpp 文件)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MyClass.h 头文件
#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
private:
int data;

public:
MyClass(int value);
void display();
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
// 实现文件 MyClass.cpp
#include <iostream>
#include "MyClass.h"
using namespace std;

// 构造函数的实现,使用初始化列表初始化成员变量 data
MyClass::MyClass(int value) : data(value) {}

// display 成员函数的实现
void MyClass::display() {
cout << "Data: " << data << endl;
}

**构造函数 MyClass::MyClass(int value)**:

  • 使用初始化列表 : data(value)value 的值赋给成员变量 data
  • MyClass:: 指明这是 MyClass 类的构造函数。

**成员函数 MyClass::display()**:

  • 输出 data 的值。
  • MyClass:: 表示该函数属于 MyClass 类。
    1
    2
    3
    4
    5
    6
    7
    8
    // 主函数
    #include "MyClass.h"

    int main() {
    MyClass obj(10);
    obj.display();
    return 0;
    }

声明与实现分离的优点

  1. 代码清晰:

    • 类的接口和实现分开,头文件只展示接口,源文件包含具体实现。
  2. 复用性高:

    • 头文件可以被多个源文件包含,而实现文件只需编译一次。
  3. 便于维护:

    • 修改实现时,不需要重新编译依赖头文件的其他文件。

C++ 结构体 struct

C语言和C++ 都有结构体,语法也基本相似,但是 C++ 是面向对象的编程语言,在 C语言结构体之上做了扩展,赋予了更多的功能。C++ 的结构体基本上就相当于 类(class)。

访问控制

  • 在 C++ 中,结构体的成员默认是 public 权限。
  • 在 C 语言中,没有访问控制的概念,所有成员都相当于是 public 。
1
2
3
4
5
struct CppStruct {
int x; // 默认是 public
private:
int y; // 必须显示声明私有成员变量
};
1
2
3
4
struct CStruct {
int x;
int y; //所有成员变量都是 public
};

支持成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

struct CppStruct {
int x;
void display() { // 成员函数
std::cout << "x = " << x << std::endl;
}
};

int main() {
CppStruct s;
s.x = 10;
s.display();
return 0;
}

C 语言中并不能直接将函数封装到结构体中,但是可以通过函数指针的形式来达到类似的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

struct CStruct {
int x;
void (*display)(struct CStruct*); // 函数指针作为成员变量。
};

void displayFunction(struct CStruct* self) {
printf("x = %d\n", self->x);
}

int main() {
struct CStruct obj;
obj.x = 99;
obj.display = displayFunction; // 将函数指针赋值为具体函数
obj.display(&obj); // 调用 "成员函数"
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct CStruct {
int x;
void (*setX)(struct CStruct*, int);
void (*display)(struct CStruct*);
};

void setXFunction(struct CStruct* self, int value) {
self->x = value;
}

void displayFunction(struct CStruct* self) {
printf("x = %d\n", self->x);
}

int main() {
struct CStruct obj;

// 初始化函数指针
obj.setX = setXFunction;
obj.display = &displayFunction;

// 调用“成员函数”
obj.setX(&obj, 100);
obj.display(&obj);

return 0;
}

指向函数的指针定义形式:函数的类型标识符 (*指针变量名)(形参类型表);
指向函数的指针赋值:指向函数的指针变量名 = 函数名; 或加上&取地址运算符: 指向函数的指针变量 = &函数名;


C++ 结构体支持继承和多态

C++ 中的结构体和类没有本质的区别,结构体可以继承其他类或结构体,并支持虚函数和多态。C语言的结构体并没有这些特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Base {     // 定义一个 Base 类
int x;
virtual void show() { // 虚函数
std::cout << "Base x = " << x << std::endl;
}
//虚函数的作用是允许在派生类中重写它,并支持运行时多态(动态绑定)。
};

struct Derived : public Base { // Derived 类 继承 Base 类
void show() override { // 重写虚函数
std::cout << "Derived x = " << x << std::endl;
}
};

Derived 结构体(类)继承方式为 **public**,因此 Base 中的 public 成员在 Derived 中仍然是 **public**。

重写了基类的虚函数 show()

  • 使用 override 关键字显式声明,确保这是对基类虚函数的重写。
  • 输出信息修改为 Derived x = ...,以表示该函数来自 Derived 类。

虚函数和多态的实现

  • 虚函数 是支持运行时多态的关键。

  • 当通过 基类指针或引用 调用虚函数时,会根据对象的实际类型调用对应的版本,而不是编译时决定调用哪一个函数。这种机制称为 动态绑定

  • 如果虚函数没有被标记为 virtual,调用函数的版本将在编译时确定,表现为 静态绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

struct Base {
int x;
virtual void show() {
cout << "Base x = " << x << endl;
}
};

struct Derived : public Base {
void show() override {
cout << "Derived x = " << x << endl;
}
};

int main() {
Base* basePtr; // 定义一个指向 Base 的指针
Derived derivedObj; // 创建 Derived 类的对象
derivedObj.x = 10; // 设置 x 的值

basePtr = &derivedObj; // 基类指针指向派生类对象

basePtr->show(); // 动态绑定,调用 Derived 类的 show()
//运行:Derived x = 10
return 0;
}

basePtr 是一个指向 Base 类型的指针。它指向了一个 Derived 类型的对象(derivedObj)。当调用 basePtr->show() 时,由于 show() 是虚函数,调用会在运行时动态决定。即使指针的类型是 Base*,但它指向的是一个 Derived 对象,因此调用的是 Derivedshow() 函数。

如果没有将 Base::show 声明为 virtual,调用后输出结果为 Base x = 10。非虚函数调用是 静态绑定 的,函数的调用在编译时决定。即使 basePtr 指向的是 Derived 对象,调用的仍然是 Base::show


在 C++ 中,结构体(struct)和类(class)除了默认访问权限不同外,也没什么太大区别。

  • struct 的默认访问权限是 public
  • class 的默认访问权限是 private

除了默认访问权限,结构体和类在功能上完全相同。都可以定义成员变量和成员函数,支持继承,定义虚函数实现多态。


C++ 结构体支持静态成员

C++ 结构体可以有静态成员变量和静态成员函数。

静态成员变量

  • 静态成员变量是用 static 关键字修饰的变量。

  • 它属于 类(或结构体)本身,而不是某个特定的对象。

  • 所有对象共享同一个静态成员变量,无论创建多少个对象,静态成员变量只有一份。

1
2
3
4
5
6
7
8
9
// C++ 示例
struct MyStruct {
static int count; // 静态成员变量
static void displayCount() { // 静态成员函数
std::cout << "Count = " << count << std::endl;
}
};

int MyStruct::count = 0; // 初始化静态变量

静态成员变量的特点

  1. 全局唯一性
    • 静态成员变量存储在全局(或静态)内存区域,而不是在对象的内存区域中。
    • 不随对象的创建或销毁而自动创建或销毁。
  2. 需要在类外初始化
    • 静态成员变量必须在结构体外部进行显式定义和初始化。
    • 定义时不能使用 static 关键字。
  3. 可以直接通过类名访问
    • 静态成员变量可以通过 类名对象 访问。
    • 常用 结构体名::静态变量名 的形式。
  4. 与普通成员变量的区别
    • 普通成员变量每个对象都有独立的一份,静态成员变量是共享的。
    • 静态成员变量不依赖于对象,可以在没有创建对象的情况下访问。
  5. 作用域和生存期
    • 静态成员变量的作用域是类的作用域。
    • 它的生命周期贯穿整个程序的运行期。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
using namespace std;

struct MyStruct {
static int count; // 静态成员变量声明
int id;

MyStruct(int value) : id(value) {
count++; // 每创建一个对象,计数加一
}

~MyStruct() {
count--; // 每销毁一个对象,计数减一
}

static void showCount() { // 静态成员函数
cout << "Current count: " << count << endl;
}
};

// 静态成员变量定义和初始化
int MyStruct::count = 0;

int main() {
MyStruct::showCount(); // 输出 0,因为还没有创建对象

MyStruct obj1(1);
MyStruct::showCount(); // 输出 1

MyStruct obj2(2);
MyStruct::showCount(); // 输出 2

{
MyStruct obj3(3);
MyStruct::showCount(); // 输出 3
} // obj3 在此处销毁

MyStruct::showCount(); // 输出 2

return 0;
}

静态成员函数

  • 静态成员函数是用 static 关键字修饰的类成员函数。
  • 它属于 类本身,而不是某个特定的对象。
  • 静态成员函数可以直接访问静态成员变量,但不能直接访问非静态成员变量或成员函数。

静态成员函数的特点

  1. 无需对象即可调用
    • 静态成员函数可以通过类名直接调用,无需创建类的对象。
    • 形式为 ClassName::StaticFunctionName()
  2. 与对象无关
    • 静态成员函数不能直接访问类的非静态成员(包括成员变量和成员函数),因为它与具体对象无关。
    • 它只能访问静态成员变量和其他静态成员函数。
  3. 没有 this 指针
    • 静态成员函数没有隐式的 this 指针,因为它不属于任何对象。
  4. 常用于全局共享逻辑
    • 静态成员函数通常用于实现一些与类关联的全局逻辑,比如处理静态成员变量的操作或提供某些工具方法。

静态成员函数的特点

  • 不能访问非静态成员:
    • 静态成员函数只能直接访问静态成员变量和其他静态成员函数。
    • 如果需要访问非静态成员,可以通过传递对象的引用或指针来实现。
  • 不能使用 this 指针:
    • 因为静态成员函数不依赖于任何对象实例,所以它无法使用 this 指针。
  • 无法是虚函数
    • 静态成员函数不能声明为虚函数,因为虚函数依赖于对象的动态绑定,而静态成员函数与对象无关。

C++ 结构体支持构造函数、析构函数 和 拷贝构造函数

1
2
3
4
5
6
7
8
9
10
// C++ 示例
struct MyStruct {
int x;
MyStruct(int val) : x(val) { // 构造函数
std::cout << "MyStruct initialized with x = " << x << std::endl;
}
~MyStruct() { // 析构函数
std::cout << "MyStruct destroyed" << std::endl;
}
};



例子:

Shape.h 头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#ifndef SHAPE_H
#define SHAPE_H

#include <iostream>
#include <cstring> // 用于字符串操作

// 基类:Shape(抽象类)
class Shape {
protected:
char* name; // 动态分配的名称(需要深拷贝处理)

public:
// 构造函数
Shape(const char* shapeName);

// 拷贝构造函数(深拷贝)
Shape(const Shape& other);

// 虚析构函数
virtual ~Shape();

// 纯虚函数:计算面积
virtual double area() const = 0;

// 常量成员函数:获取名称
const char* getName() const;

// 虚函数:打印信息
virtual void print() const;
};

// 派生类:Rectangle(继承自 Shape)
class Rectangle : public Shape {
private:
double width;
double height;

public:
// 构造函数
Rectangle(const char* name, double w, double h);

// 覆写虚函数 area()
double area() const override;

// 覆写虚函数 print()
void print() const override;
};

// 结构体:Point(包含静态成员)
struct Point {
static int count; // 静态成员,记录 Point 实例的数量
double x;
double y;

Point(double x, double y);
~Point(); // 析构函数
};

#endif // SHAPE_H

实现文件 Shape.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include "Shape.h"
#include <iostream>
#include <cstring> // 用于字符串操作

using namespace std;

// 静态成员初始化
int Point::count = 0;

// ----------------- Point 结构体 -----------------
Point::Point(double x, double y) : x(x), y(y) {
count++;
cout << "Point created (" << x << ", " << y << "). Total Points: " << count << endl;
}

Point::~Point() {
count--;
cout << "Point destroyed. Total Points: " << count << endl;
}

// ----------------- Shape 类 -----------------
Shape::Shape(const char* shapeName) {
name = new char[strlen(shapeName) + 1];
strcpy(name, shapeName);
cout << "Shape constructor called for " << name << endl;
}

Shape::Shape(const Shape& other) {
name = new char[strlen(other.name) + 1];
strcpy(name, other.name);
cout << "Shape copy constructor called for " << name << endl;
}

Shape::~Shape() {
cout << "Shape destructor called for " << name << endl;
delete[] name;
}

const char* Shape::getName() const {
return name;
}

void Shape::print() const {
cout << "Shape: " << name << endl;
}

// ----------------- Rectangle 类 -----------------
Rectangle::Rectangle(const char* name, double w, double h)
: Shape(name), width(w), height(h) {
cout << "Rectangle constructor called for " << name << endl;
}

double Rectangle::area() const {
return width * height;
}

void Rectangle::print() const {
cout << "Rectangle: " << name << ", Width: " << width << ", Height: " << height
<< ", Area: " << area() << endl;
}

主程序:main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "Shape.h"
#include <iostream>

using namespace std;

int main() {
cout << "--- Create Points ---" << endl;
Point p1(1.0, 2.0);
Point p2(3.0, 4.0);
cout << "Total Points: " << Point::count << endl;

cout << "\n--- Create Rectangle ---" << endl;
Rectangle rect("MyRectangle", 5.0, 3.0);
rect.print();

cout << "\n--- Test Shape Copy ---" << endl;
Rectangle rectCopy = rect; // 调用拷贝构造函数
rectCopy.print();

cout << "\n--- Test Polymorphism ---" << endl;
Shape* shape = new Rectangle("PolymorphicRectangle", 7.0, 4.0);
shape->print();
cout << "Area: " << shape->area() << endl;
delete shape; // 确保调用虚析构函数

cout << "\n--- End Program ---" << endl;

return 0;
}

编译:

1
2
3
jackeysong@jackey-song:~/Desktop/cpp/example$ g++ -o main main.cpp Shape.cpp
jackeysong@jackey-song:~/Desktop/cpp/example$ ls
main main.cpp Shape.cpp Shape.h

运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ackeysong@jackey-song:~/Desktop/cpp/example$ ./main
--- Create Points ---
Point created (1, 2). Total Points: 1
Point created (3, 4). Total Points: 2
Total Points: 2

--- Create Rectangle ---
Shape constructor called for MyRectangle
Rectangle constructor called for MyRectangle
Rectangle: MyRectangle, Width: 5, Height: 3, Area: 15

--- Test Shape Copy ---
Shape copy constructor called for MyRectangle
Rectangle: MyRectangle, Width: 5, Height: 3, Area: 15

--- Test Polymorphism ---
Shape constructor called for PolymorphicRectangle
Rectangle constructor called for PolymorphicRectangle
Rectangle: PolymorphicRectangle, Width: 7, Height: 4, Area: 28
Area: 28
Shape destructor called for PolymorphicRectangle

--- End Program ---
Shape destructor called for MyRectangle
Shape destructor called for MyRectangle
Point destroyed. Total Points: 1
Point destroyed. Total Points: 0



本篇文章内容已经过长了,对于 C++ 内容的补充会在以后得文章中写。

Prev:
如何在电脑上同时安装 Windows 和 Linux 双系统?
Next:
解决Ubuntu下无法装载 Windows D盘的问题