C++(1)之基础语法
Author: Once Day Date: 2024年8月29日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章可参考专栏: 源码分析_Once-Day的博客-CSDN博客
参考文章:
- C++ 基本语法_w3cschool
- Learn C++ – Skill up with our free tutorials (learncpp.com)
- 词法约定 | Microsoft Learn
- GitHub - isocpp/CppCoreGuidelines: The C++ Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about
coding in C++
文章目录
- C++(1)之基础语法
- 1. 介绍
- 1.1 简介
- 1.2 示例
- 1.3 编译
- 1.4 与C的联系和区别
- 2. 基础数据
- 2.1 标识符和关键字
- 2.2 注释
- 2.3 基础类型
- 2.4 枚举类型
- 2.5 变量初始化
- 2.6 字面常量和符号常量
- 2.7 左值和右值
- 2.8 操作运算符
- 2.9 地址对齐
- 2.10 类型转换
- 2.11 变量作用域
- 3. 复合数据
- 3.1 C风格字符串和string字符串
- 3.2 数组
- 3.3 结构体与类
- 3.3 联合体
- 4. 语句和表达式
- 4.1 常见表达式
- 4.2 常见控制语句
- 4.3 基本函数
- 4.4 模版函数
- 4.5 常量函数
- 5. 基本特性
- 5.1 内存模型
- 5.2 存储和链接
- 5.4 名称空间
- 6. 基础总结
1. 介绍
1.1 简介
C++是一种通用编程语言,由Bjarne Stroustrup在贝尔实验室工作时于1979年开始开发。C++最初被命名为"C with Classes",旨在扩展C语言以支持面向对象编程。随着时间的推移,C++不断发展,引入了许多新特性,如异常处理、模板、标准模板库(STL)等。1998年,C++第一个国际标准ISO/IEC 14882:1998发布,使其成为一种成熟、稳定且广泛使用的编程语言。此后,C++又经历了多次标准化更新,包括C++03、C++11、C++14、C++17和C++20,不断完善语言特性,提高性能和可用性,使其在系统编程、游戏开发、嵌入式系统等领域得到广泛应用。
1.2 示例
下面是一个简单的C++程序示例,用于计算两个整数的和:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int b = 20;
int sum = a + b;
cout << "The sum of " << a << " and " << b << " is: " << sum << endl;
return 0;
}
下面是这个程序的解释:
-
#include <iostream>
:这行代码称为预处理器指令,用于包含输入/输出流库,以便可以使用cout
进行输出。 -
using namespace std;
:这行代码表示我们使用std
命名空间,这样我们就可以直接使用cout
等对象,而无需写成std::cout
。 -
int main() { ... }
:这是程序的主函数,每个C++程序都必须有一个main
函数作为程序的入口点。int
表示main
函数返回一个整数值。 -
int a = 10;
和int b = 20;
:这两行代码声明了两个整型变量a
和b
,并分别初始化为10和20。 -
int sum = a + b;
:这行代码声明了另一个整型变量sum
,并将其初始化为a
和b
的和。 -
cout << "The sum of " << a << " and " << b << " is: " << sum << endl;
:这行代码使用cout
对象将一个字符串和变量的值输出到控制台。<<
运算符用于将多个值串联起来。endl
表示换行符。 -
return 0;
:这行代码表示main
函数返回值为0,通常表示程序正常结束。
这个程序的输出是"The sum of 10 and 20 is: 30.",非常简单,基础语法和C语言很像。
1.3 编译
C++是一种广泛使用的编程语言,有许多编译平台和集成开发环境(IDE)可供选择。以下是一些常见的C++编译平台和相关程序:
(1) GCC (GNU Compiler Collection):
- GCC是一个广泛使用的开源编译器套件,支持C++以及其他编程语言。
- 它通常与Unix-like系统(如Linux和macOS)一起使用,但也可以在Windows上使用。
- GCC通常通过命令行界面使用,但也可以与各种IDE集成。
(2) Clang:
- Clang是一个基于LLVM的开源编译器,旨在提供快速的编译速度和有用的错误消息。
- 它与GCC兼容,支持C++以及其他编程语言。
- Clang通常在Unix-like系统上使用,也是macOS上Xcode IDE的默认编译器。
(3) Microsoft Visual C++ (MSVC):
- MSVC是Microsoft Visual Studio IDE的一部分,是Windows平台上常用的C++编译器。
- 它提供了广泛的开发工具和库,用于构建Windows应用程序。
- MSVC与Windows SDK集成,支持Windows特定的功能和API。
(4) Intel C++ Compiler (ICC):
- ICC是由Intel开发的高性能C++编译器,针对Intel处理器进行了优化。
- 它提供了高级优化功能,可生成高度优化的代码。
- ICC可以在Windows、Linux和macOS上使用。
(5) 集成开发环境(IDE):
- Visual Studio:由Microsoft开发,是Windows平台上广泛使用的IDE,提供了强大的开发工具和调试功能。
- Eclipse CDT:基于Eclipse平台的开源IDE,支持C++开发,可在多个操作系统上使用。
- Code::Blocks:一个开源的跨平台IDE,提供了简单直观的界面和各种编译器支持。
- CLion:由JetBrains开发的跨平台IDE,提供了智能代码辅助和高级调试功能。
- Xcode:由Apple开发,是macOS上的集成开发环境,提供了用于开发macOS和iOS应用程序的工具。
选择编译器和IDE通常取决于目标平台、个人偏好以及项目的具体需求。许多IDE都支持多个编译器,允许开发人员根据需要进行切换。
1.4 与C的联系和区别
C++是在C语言的基础上开发而来的,因此它们之间存在许多相同点(注意,C和C++始终是两门语言,不能一起混用)。
-
C++是C的超集:几乎所有的C语法都是C++语法的一部分。大多数C程序可以使用C++编译器进行编译而无需修改。
-
相似的语法:C++保留了C的基本语法,如控制结构、函数声明、基本数据类型等。
-
相同的底层概念:C++和C都提供了对指针、内存管理和低级操作的支持。
-
标准库兼容:C++可以使用C标准库中的函数,如
printf
、scanf
等。
此外,C++在C的基础上添加了许多新特性,这也导致其特别复杂,学习的主要难点都在于这些新特性:
-
面向对象编程(OOP):C++引入了类、对象、继承、多态等面向对象的概念,而C是一种过程式编程语言。
-
函数重载:C++支持函数重载,允许多个同名函数具有不同的参数类型或数量,而C不支持函数重载。
-
异常处理:C++提供了异常处理机制,使用
try
、catch
和throw
关键字来处理异常,而C没有内置的异常处理机制。 -
引用:C++引入了引用的概念,可以创建变量的别名,而C没有引用。
-
运算符重载:C++允许重载运算符,赋予运算符新的含义,而C不支持运算符重载。
-
命名空间:C++引入了命名空间,用于避免命名冲突和实现模块化编程,而C没有命名空间。
-
模板:C++引入了模板,允许编写泛型代码,而C不支持模板。
-
输入/输出:C++引入了
cin
和cout
等输入/输出流对象,提供了更方便、类型安全的输入/输出操作,而C使用scanf
和printf
等函数进行输入/输出。 -
内存分配:C++引入了
new
和delete
运算符用于动态内存分配和释放,而C使用malloc
和free
函数进行动态内存管理。 -
标准库:C++提供了更丰富、更强大的标准库,如STL(标准模板库),提供了大量的通用数据结构和算法,而C的标准库相对较小。
2. 基础数据
2.1 标识符和关键字
参考链接:
- 标记和字符集 | Microsoft Learn,
- C++ 基本语法_w3cschool
- 关键字 (C++) | Microsoft Learn
在C++中,标识符是用于标识变量、函数、类、对象等实体的名称。
- 标识符由字母(a-z、A-Z)、数字(0-9)和下划线(_)组成。
- 必须以字母或下划线开头。
- C++标识符区分大小写。
以下是C++中的关键字列表,这些关键字被语言保留,不能用作标识符:
关键字 | 描述 |
---|---|
asm | 用于在C++程序中嵌入汇编语言代码 |
auto | 自动推断变量的数据类型 |
bool | 布尔数据类型,取值为true或false |
break | 跳出当前循环或switch语句 |
case | 用于switch语句中的标签 |
catch | 用于异常处理,捕获抛出的异常 |
char | 字符数据类型 |
class | 定义一个类 |
const | 声明一个常量,其值不能被修改 |
const_cast | 用于移除表达式的const或volatile限定符 |
continue | 跳过当前循环的剩余部分,开始下一次循环 |
default | 用于switch语句中的默认分支 |
delete | 释放动态分配的内存 |
do | 用于do-while循环 |
double | 双精度浮点数据类型 |
dynamic_cast | 执行运行时多态类型转换 |
else | 用于if语句中的替代分支 |
enum | 定义一个枚举类型 |
explicit | 显式构造函数,防止隐式类型转换 |
export | 用于将模板声明为已导出 |
extern | 声明一个外部变量或函数 |
false | 布尔字面量,表示假 |
float | 单精度浮点数据类型 |
for | 用于for循环 |
friend | 声明一个友元函数或类 |
goto | 无条件跳转到指定的标签 |
if | 用于条件语句 |
inline | 内联函数,建议编译器将函数内联展开 |
int | 整数数据类型 |
long | 长整数数据类型 |
mutable | 允许修改类的const成员变量 |
namespace | 定义一个命名空间 |
new | 动态分配内存 |
noexcept | 指定函数不抛出异常 |
nullptr | 空指针字面量 |
operator | 重载运算符 |
private | 类的私有访问说明符 |
protected | 类的保护访问说明符 |
public | 类的公有访问说明符 |
register | 建议编译器将变量存储在寄存器中 |
reinterpret_cast | 执行低级别的强制类型转换 |
return | 从函数返回值 |
short | 短整数数据类型 |
signed | 有符号数据类型 |
sizeof | 返回一个对象或类型的大小 |
static | 声明静态变量或函数 |
static_assert | 在编译时断言一个条件为真 |
static_cast | 执行编译时类型转换 |
struct | 定义一个结构体 |
switch | 用于多分支选择语句 |
template | 定义一个模板 |
this | 指向当前对象的指针 |
thread_local | 声明线程局部存储的变量 |
throw | 抛出一个异常 |
true | 布尔字面量,表示真 |
try | 用于异常处理,尝试执行可能抛出异常的代码块 |
typedef | 定义一个类型别名 |
typeid | 获取表达式的类型信息 |
typename | 用于模板中指定类型参数 |
union | 定义一个联合体 |
unsigned | 无符号数据类型 |
using | 引入命名空间或创建类型别名 |
virtual | 声明虚函数或虚基类 |
void | 表示函数没有返回值或指针指向未知类型 |
volatile | 声明一个易变的变量,防止编译器优化 |
wchar_t | 宽字符数据类型 |
while | 用于while循环 |
除了这些关键字,C++还有一些预处理器指令和特殊标识符,如:
#include
、#define
、#ifdef
、#ifndef
、#endif
等预处理器指令__LINE__
、__FILE__
、__DATE__
、__TIME__
等特殊标识符
需要注意的是,C++标准库中的标识符,如cin
、cout
、endl
、vector
、string
等,虽然不是关键字,但也有特殊的用途,通常不建议将它们用作普通标识符。
在选择标识符名称时,应遵循以下规则:
- 标识符应具有描述性,清晰表达其用途或含义。
- 避免使用保留关键字作为标识符。
- 遵循一致的命名约定,如驼峰式命名法(camelCase)或下划线命名法(snake_case)。
- 避免使用过于简短或过于冗长的标识符。
- 对于常量和宏,通常使用全大写字母和下划线的命名方式。
合理地选择和使用标识符可以提高代码的可读性和可维护性。
2.2 注释
参考链接:注释 (C++) | Microsoft Learn,C++ 注释_w3cschool
C++支持两种注释风格:单行注释和多行注释。
(1) 单行注释:
-
单行注释以
//
开头,后面跟着注释文本。 -
单行注释从
//
开始,直到该行的末尾。 -
单行注释可以独占一行,也可以放在代码语句的末尾。
// 这是一个单行注释 int x = 10; // 这也是一个单行注释
(2) 多行注释:
-
多行注释以
/*
开头,以*/
结尾,注释文本放在这两个分隔符之间。 -
多行注释可以跨越多行,直到遇到结束分隔符
*/
。 -
多行注释通常用于较长的解释性文本或临时禁用一段代码。
/* 这是一个 跨越多行的 注释 */
C++注释风格与C语言的区别:
- C++引入了单行注释
//
,而C语言在C99标准之前没有单行注释。 - C语言只支持多行注释
/* */
,而C++同时支持单行注释和多行注释。 - C++的单行注释提供了一种更方便、更灵活的注释方式,特别适用于简短的解释性文本。
需要注意的是,在使用多行注释时,不能嵌套多行注释。例如,以下代码将导致编译错误:
/* 外层注释
/* 嵌套的内层注释 */
外层注释结束 */
此外,在注释中使用适当的格式和符号可以提高注释的可读性,例如:
// 函数说明:计算两个数的和
// 参数:
// - a: 第一个数
// - b: 第二个数
// 返回值:两个数的和
int sum(int a, int b) {
return a + b;
}
2.3 基础类型
参考文档:
- C++ 数据类型_w3cschool
- 内置类型 (C++) | Microsoft Learn
- C++ 类型系统 | Microsoft Learn
下面是C++的基础类型:
类型名 | 取值范围 | 描述 |
---|---|---|
bool | true或false | 布尔类型,表示真或假 |
char | -128到127或0到255 | 字符类型,通常占用1个字节 |
signed char | -128到127 | 有符号字符类型 |
unsigned char | 0到255 | 无符号字符类型 |
wchar_t | 实现定义 | 宽字符类型,通常占用2个或4个字节 |
char16_t | 0到65,535 | 16位Unicode字符类型 (C++11) |
char32_t | 0到4,294,967,295 | 32位Unicode字符类型 (C++11) |
short | -32,768到32,767 | 短整型,通常占用2个字节 |
unsigned short | 0到65,535 | 无符号短整型 |
int | -2,147,483,648到2,147,483,647 | 整型,通常占用4个字节 |
unsigned int | 0到4,294,967,295 | 无符号整型 |
long | -2,147,483,648到2,147,483,647 | 长整型,通常占用4个字节(在64位系统上可能占用8个字节) |
unsigned long | 0到4,294,967,295 | 无符号长整型 |
long long | -9,223,372,036,854,775,808到9,223,372,036,854,775,807 | 长长整型,通常占用8个字节 (C++11) |
unsigned long long | 0到18,446,744,073,709,551,615 | 无符号长长整型 (C++11) |
float | 3.4E-38到3.4E+38 (6位有效数字) | 单精度浮点数,通常占用4个字节 |
double | 1.7E-308到1.7E+308 (15位有效数字) | 双精度浮点数,通常占用8个字节 |
long double | 实现定义 (通常为1.7E-4932到1.7E+4932,18-21位有效数字) | 长双精度浮点数,通常占用8、12或16个字节 |
需要注意的是,不同的计算机架构和编译器实现可能会有所不同,因此类型的取值范围可能会有所变化。上述取值范围是基于常见的实现。
2.4 枚举类型
参考文档:枚举 (C++) | Microsoft Learn
枚举类型(enumeration type)是C++中一种用户自定义的数据类型,用于表示一组离散的命名常量。枚举类型提供了一种有意义的方式来表示一组相关的常量值。
(1) C/C++经典枚举类型:
-
使用
enum
关键字定义一个枚举类型。 -
枚举类型的命名常量称为枚举值(enumerator)。
-
枚举值默认从0开始,依次递增1,也可以显式指定枚举值的值。
enum Color { RED, // 默认值为0 GREEN, // 默认值为1 BLUE = 5, // 显式指定值为5 YELLOW // 默认值为6 };
-
枚举值可以隐式转换为整型,也可以进行比较操作。
-
经典枚举类型的作用域与其所在的作用域相同,枚举值可以在整个作用域内直接访问。
(2) enum class
(强类型枚举):
-
C++11引入了
enum class
(也称为强类型枚举或作用域枚举)来解决经典枚举类型的一些问题。 -
使用
enum class
关键字定义一个强类型枚举。 -
强类型枚举的枚举值不会隐式转换为整型,需要显式转换。
-
强类型枚举的作用域仅限于枚举类型内部,需要使用枚举类型名称和
::
运算符来访问枚举值。enum class Color { RED, GREEN, BLUE }; Color c = Color::RED; // 使用枚举类型名称和::访问枚举值
-
强类型枚举提供了更好的类型安全性和作用域控制。
(3) 指定枚举类型的底层类型:
-
在定义枚举类型时,可以使用
: type
语法指定枚举类型的底层类型。 -
底层类型可以是整型类型,如
int
、short
、long
等。 -
指定底层类型可以控制枚举类型的大小和范围。
enum class Color : int32_t { RED, GREEN, BLUE };
-
上述示例中,枚举类型
Color
的底层类型被指定为int32_t
,占用4个字节。
枚举类型的一些其他特点和用法:
- 枚举类型可以用作
switch
语句的条件,每个枚举值对应一个case
标签。 - 枚举类型可以作为函数的参数和返回值类型。
- 可以使用
static_cast
将整型显式转换为枚举类型。 - 枚举类型可以提高代码的可读性和可维护性,使代码更加自解释。
2.5 变量初始化
参考文档:初始值设定项 | Microsoft Learn
C++特有的两种初始化方法是使用圆括号()
和大括号{}
。
(1) 圆括号初始化 (Parentheses Initialization):
-
使用圆括号
()
来初始化变量或对象。 -
语法:
type variable(value);
-
对于内置类型(如
int
、double
等),圆括号初始化会进行隐式类型转换。 -
对于类类型,圆括号初始化会调用相应的构造函数。
int x(10); // 使用圆括号初始化内置类型 std::string str("Hello"); // 使用圆括号初始化字符串对象 std::vector<int> vec(5, 1); // 使用圆括号初始化vector对象,初始大小为5,元素值为1
(2) 大括号初始化 (Brace Initialization或Uniform Initialization):
-
使用大括号
{}
来初始化变量或对象。 -
语法:
type variable{value};
-
大括号初始化是C++11引入的特性,提供了一种通用的初始化语法。
-
对于内置类型,大括号初始化不允许窄化转换(narrowing conversion),即不允许潜在的精度损失。
-
对于类类型,大括号初始化会调用相应的构造函数,如果没有匹配的构造函数,则会进行聚合初始化(aggregate initialization)。
-
大括号初始化可以用于初始化列表的初始化。
int x{10}; // 使用大括号初始化内置类型 double d{1.5}; // 使用大括号初始化浮点类型 std::string str{"Hello"}; // 使用大括号初始化字符串对象 std::vector<int> vec{1, 2, 3, 4, 5}; // 使用大括号初始化vector对象,使用初始化列表
圆括号初始化和大括号初始化的一些区别和注意事项:
- 圆括号初始化允许隐式类型转换,而大括号初始化不允许窄化转换。
- 大括号初始化可以避免一些隐式类型转换导致的意外行为。
- 对于类类型,如果提供了接受初始化列表的构造函数,则大括号初始化优先调用该构造函数。
- 大括号初始化可以用于初始化聚合类型(如数组、结构体等),而圆括号初始化不行。
- 在某些情况下,大括号初始化可能会与函数声明产生歧义,需要添加额外的括号来消除歧义。
2.6 字面常量和符号常量
参考文档:
- 数值、布尔和指针文本 (C++) | Microsoft Learn
- 字符串和字符文本 (C++) | Microsoft Learn
- 用户定义的文本 (C++) | Microsoft Learn
- C++ 常量_w3cschool
在C++中,有多种类型的常量文本(literals),用于表示不同类型的常量值。
(1) 整数文本(Integer Literals):
-
语法:
[integer]
42 // 十进制整数 0xF // 十六进制整数 0b1010 // 二进制整数 (C++14)
(2) 浮点数文本(Floating-point Literals):
-
语法:
[integer].[integer][(e|E)[+|-][integer]]
3.14 // 双精度浮点数 2.5f // 单精度浮点数 1.2e-3 // 科学记数法表示的双精度浮点数
(3) 布尔文本(Boolean Literals):
-
语法:
true
或false
true // 布尔值真 false // 布尔值假
(4) 字符文本(Character Literals):
-
语法:
'[character]'
'a' // 字符文本 '\n' // 转义字符文本 (换行符) '\x1F' // 十六进制转义字符文本
(5) 字符串文本(String Literals):
-
语法:
"[characters]"
"Hello, world!" // 字符串文本 "Hello\nworld!" // 包含转义字符的字符串文本 R"(Raw string literal)" // 原始字符串文本 (C++11)
(6) 指针文本(Pointer Literals):
-
语法:
nullptr
nullptr // 空指针文本 (C++11)
(7) 用户定义文本(User-defined Literals):
-
语法:
[integer|floating-point|string|character]_[suffix]
1234_km // 整数文本with km后缀 3.14_rad // 浮点数文本with rad后缀 "Hello"_s // 字符串文本with s后缀
-
用户定义文本允许用户自定义后缀,并通过重载
operator""
函数来实现自定义文本的解析和转换。
除了上述常见的常量文本类型,C++还支持其他一些特殊的常量文本,如宽字符文本(L'x'
)、宽字符串文本(L"hello"
)、Unicode字符文本(u8'x'
,u'x'
,U'x'
)和Unicode字符串文本(u8"hello"
, u"hello"
,U"hello"
)等。
2.7 左值和右值
参考文档:值类别:lvalue 和 rvalue (C++) | Microsoft Learn
在C++中,表达式可以分为左值(lvalue)和右值(rvalue)两种类型。左值和右值的区别在于它们的生命周期和可以执行的操作。
(1) 左值(lvalue):
-
左值是一个表达式,它引用了一个持久的对象,可以出现在赋值运算符的左侧。
-
左值有稳定的内存地址,可以被取地址运算符
&
获取其地址。 -
左值可以被赋值,也可以被修改。
int x = 10; // x是一个左值,可以被赋值 int& ref = x; // ref是一个左值引用,引用了左值x int* ptr = &x; // 可以对左值x取地址
(2) 右值(rvalue):
-
右值是一个表达式,它引用了一个临时的对象,通常出现在赋值运算符的右侧。
-
右值没有持久的内存地址,不能被取地址运算符
&
获取其地址。 -
右值通常是临时的,不能被直接修改。
int x = 10; int y = x + 5; // x + 5是一个右值,表示一个临时的值 int& ref = x + 5; // 错误:不能将右值绑定到非常量左值引用 const int& cref = x + 5; // 正确:可以将右值绑定到常量左值引用
(3) 右值引用(rvalue reference):
-
C++11引入了右值引用的概念,用于捕获和操作右值。
-
右值引用使用
&&
符号声明,如int&&
表示一个右值引用。 -
右值引用可以延长右值的生命周期,允许对右值进行修改。
-
右值引用主要用于实现移动语义和完美转发。
int&& rref = 10; // rref是一个右值引用,绑定到右值10 rref = 20; // 可以通过右值引用修改右值
(3) 移动语义和移动构造函数:
-
移动语义允许将资源从一个对象移动到另一个对象,而不是进行昂贵的复制操作。
-
移动构造函数是一种特殊的构造函数,用于实现移动语义。
-
移动构造函数接受一个右值引用参数,用于转移资源的所有权。
class String { public: String(String&& other) { // 移动构造函数 data_ = other.data_; other.data_ = nullptr; } // ... private: char* data_; };
(4) 完美转发:
-
完美转发是一种技术,允许函数模板将其参数按照原始类型(左值或右值)转发给另一个函数。
-
完美转发使用
std::forward
函数模板来保持参数的值类别(左值或右值)。template <typename T> void forwardValue(T&& value) { processValue(std::forward<T>(value)); }
左值和右值的区别在于它们的生命周期和可以执行的操作。左值有持久的内存地址,可以被赋值和修改;右值通常是临时的,不能被直接修改。
2.8 操作运算符
参考文档:
- C++ 运算符_w3cschool
- C++ 内置运算符、优先级和关联性 | Microsoft Learn
以下是常见的C++算术运算符表格:
运算符 | 名称 | 描述 | 示例 |
---|---|---|---|
+ | 加法 | 将两个操作数相加 | int result = a + b; |
- | 减法 | 将第一个操作数减去第二个操作数 | int result = a - b; |
* | 乘法 | 将两个操作数相乘 | int result = a * b; |
/ | 除法 | 将第一个操作数除以第二个操作数 | int result = a / b; |
% | 取模/取余 | 返回第一个操作数除以第二个操作数的余数 | int result = a % b; |
++ | 自增 | 将操作数的值增加1 | ++a; 或 a++; |
-- | 自减 | 将操作数的值减少1 | --a; 或 a--; |
+ | 一元加 | 返回操作数的正值 | int result = +a; |
- | 一元减 | 返回操作数的相反值 | int result = -a; |
这些运算符的行为可能因操作数的类型而有所不同:
- 对于整数类型,
/
运算符执行整数除法,结果为商的整数部分。例如,7 / 2
的结果为3
。 - 对于浮点类型,
/
运算符执行浮点除法,结果为精确的商。例如,7.0 / 2.0
的结果为3.5
。 - 取模运算符
%
只适用于整数类型,返回整数除法的余数。例如,7 % 2
的结果为1
。 - 自增和自减运算符可以用作前缀(
++a
,--a
)或后缀(a++
,a--
)。前缀形式先增加/减少值,然后使用新值;后缀形式先使用当前值,然后增加/减少值。
除了算术运算符,C++还提供了许多其他类型的运算符。
类别 | 运算符 | 描述 |
---|---|---|
赋值运算符 | = , += , -= , *= , /= , %= , &= , |= , ^= , <<= , >>= | 将右侧的值赋给左侧的变量,或者将运算符右侧的值与左侧的变量进行相应的运算,并将结果赋给左侧的变量 |
比较运算符 | == , != , < , > , <= , >= | 比较两个操作数的相等性或大小关系 |
逻辑运算符 | && , || , ! | 对布尔表达式进行逻辑运算 |
位运算符 | & , | , ^ , ~ , << , >> | 对整数类型的二进制位进行位运算 |
条件运算符 | ? : | 根据条件表达式的值选择其中一个表达式的值 |
逗号运算符 | , | 按从左到右的顺序计算表达式,返回最右边的表达式的值 |
成员访问运算符 | . , -> | 用于访问对象的成员 |
指针运算符 | * , & | 用于访问指针指向的对象或获取变量的内存地址 |
类型转换运算符 | static_cast , dynamic_cast , const_cast , reinterpret_cast | 用于执行显式类型转换 |
sizeof运算符 | sizeof | 返回对象或类型的大小(以字节为单位) |
2.9 地址对齐
参考文档:
- 保持同步 | Microsoft Learn
- alignof 运算符 | Microsoft Learn
C++中的地址对齐(alignment)是指将数据对象的地址调整为特定边界的倍数,以优化内存访问性能和避免硬件异常。每种数据类型都有其默认的对齐要求,通常与数据类型的大小相关。
C++11引入了alignof
和alignas
关键字来处理地址对齐:
(1) alignof
运算符:
-
alignof
运算符用于获取类型或表达式的对齐要求。 -
语法:
alignof(type)
或alignof(expression)
。 -
返回值: 一个
std::size_t
类型的值,表示类型或表达式的对齐要求(以字节为单位)。#include <iostream> struct MyStruct { char c; int i; double d; }; int main() { std::cout << "Alignment of int: " << alignof(int) << std::endl; std::cout << "Alignment of double: " << alignof(double) << std::endl; std::cout << "Alignment of MyStruct: " << alignof(MyStruct) << std::endl; int x; std::cout << "Alignment of x: " << alignof(x) << std::endl; return 0; }
输出:
Alignment of int: 4 Alignment of double: 8 Alignment of MyStruct: 8 Alignment of x: 4
(2) alignas
说明符:
-
alignas
说明符用于指定变量、类成员或类型的自定义对齐要求。 -
语法:
alignas(alignment) type name;
或alignas(type) name;
。 -
alignment
必须是2的幂,否则编译器会将其调整为下一个有效的对齐值。#include <iostream> struct alignas(16) MyAlignedStruct { char c; int i; }; int main() { MyAlignedStruct s; std::cout << "Alignment of MyAlignedStruct: " << alignof(MyAlignedStruct) << std::endl; std::cout << "Address of s: " << static_cast<void*>(&s) << std::endl; alignas(32) int x; std::cout << "Alignment of x: " << alignof(x) << std::endl; std::cout << "Address of x: " << static_cast<void*>(&x) << std::endl; return 0; }
输出:
Alignment of MyAlignedStruct: 16 Address of s: 0x7ffd2ebaff00 Alignment of x: 32 Address of x: 0x7ffd2ebaff20
正确处理地址对齐可以提高内存访问的效率,特别是在处理大型数据结构或性能关键的代码时。但过度使用自定义对齐可能会导致内存浪费,因为编译器会在对象之间插入填充字节以满足对齐要求。在大多数情况下,使用默认的对齐方式就足够了,除非有特殊的性能要求或硬件限制。
2.10 类型转换
参考文档:
- 标准转换 | Microsoft Learn
- 类型转换和类型安全 | Microsoft Learn
C++提供了多种类型转换机制,包括隐式类型转换和显式类型转换。
(1) 类型别名,typedef
关键字和using
关键字(C++11)可以用于创建类型的别名。
typedef unsigned int uint;
using uint = unsigned int;
(2) 算术类型转换,当不同类型的算术类型一起使用时,编译器会自动执行类型提升或类型转换(隐式类型转换)。
- 整数提升: 将小于
int
的整数类型提升为int
或unsigned int
。 - 常见的算术转换: 将一个算术类型转换为另一个算术类型,以避免精度损失。
(3) 强制类型转换风格:
-
C风格的强制类型转换: 使用小括号
()
进行强制类型转换。// 语法 (new_type)expression int x = (int)3.14;
-
函数风格的强制类型转换: 使用类型名作为函数名进行强制类型转换。
// 语法 new_type(expression) int x = int(3.14);
(4) 强制类型转换运算符(C++风格):
-
static_cast
,用于进行良性的类型转换,如基本类型之间的转换、非const到const的转换等。 -
dynamic_cast
,用于在类层次结构中执行运行时的向下转换(Downcasting)。 -
const_cast
,用于移除表达式的const或volatile限定符。 -
reinterpret_cast
,用于执行低级别的强制类型转换,如指针类型之间的转换。int x = static_cast<int>(3.14); Base* b = dynamic_cast<Base*>(derived_ptr); const int x = 10; int* ptr = const_cast<int*>(&x); int* ptr = reinterpret_cast<int*>(&x);
(5) 自动类型推导:
-
auto
关键字(C++11),让编译器根据初始化表达式自动推导变量的类型。 -
decltype
关键字(C++11): 用于推导表达式的类型。auto x = 10; auto y = 3.14; int x = 10; decltype(x) y = 20;
在进行类型转换时,需要谨慎考虑转换的合理性和必要性,避免不必要的类型转换,并尽可能使用C++风格的强制类型转换运算符来提高代码的可读性和维护性。
2.11 变量作用域
参考文档:
- 声明和定义 (C++) | Microsoft Learn
- C++ 变量作用域_w3cschool
在 C++ 中,根据变量的作用域和生命周期,可以将变量分为以下几类:
(1) 局部变量(Local Variables):
- 局部变量是在函数内部或者代码块内声明的变量。
- 它们的作用域仅限于声明它们的函数或代码块内部。
- 当函数或代码块执行完毕时,局部变量会自动销毁,释放所占用的内存空间。
- 局部变量在每次函数调用时都会重新创建和初始化。
(2) 静态变量(Static Variables):
- 静态变量可以是函数内部的静态局部变量,也可以是文件作用域内的静态全局变量。
- 静态局部变量的作用域仅限于声明它们的函数内部,但是它们的生命周期与程序的生命周期相同。
- 静态全局变量的作用域仅限于声明它们的文件内部,但是它们的生命周期与程序的生命周期相同。
- 静态变量在程序开始执行时进行初始化,在程序终止时销毁。
- 静态变量只会在第一次被访问时进行初始化,之后的访问都会使用之前初始化的值。
(3) 全局变量(Global Variables):
- 全局变量是在所有函数和类之外声明的变量。
- 它们的作用域是整个程序,可以在程序的任何地方访问和修改。
- 全局变量在程序开始执行时进行初始化,在程序终止时销毁。
- 全局变量的生命周期与程序的生命周期相同。
(4) 线程局部变量(Thread-Local Variables):
- 线程局部变量是使用
thread_local
关键字声明的变量。 - 每个线程都有自己的线程局部变量副本,互不干扰。
- 线程局部变量的作用域取决于它们的声明位置,可以是局部的或全局的。
- 线程局部变量在线程开始时进行初始化,在线程终止时销毁。
(5) 临时变量(Temporary Variables):
- 临时变量是编译器在执行某些表达式时自动创建的短暂性变量。
- 临时变量的作用域通常限于单个表达式或语句。
- 临时变量在创建它们的表达式或语句执行完毕后就会自动销毁。
(6) 变量的构造和析构流程:
- 对于局部变量,在进入它们的作用域时进行构造,在离开作用域时进行析构。
- 对于静态变量和全局变量,在程序开始执行时进行构造,在程序终止时进行析构。
- 对于线程局部变量,在线程开始时进行构造,在线程终止时进行析构。
- 对于临时变量,在表达式或语句执行时进行构造,在表达式或语句执行完毕后进行析构。
需要注意的是,对于局部变量和临时变量,如果它们是类的对象,那么它们的构造函数和析构函数会在相应的时间点被自动调用。
而对于静态变量、全局变量和线程局部变量,如果它们是类的对象,那么它们的构造函数会在程序或线程开始时被调用,析构函数会在程序或线程终止时被调用。
3. 复合数据
3.1 C风格字符串和string字符串
参考文档:C++ 字符串_w3cschool
在 C++ 中,有两种主要的字符串表示方式C 风格字符串和 string
字符串。
(1) C 风格字符串:
- C 风格字符串是一个以空字符
'\0'
结尾的字符数组。 - 字符数组可以使用字符串字面量进行初始化,如
char str[] = "Hello";
。 - 字符数组的大小要足够容纳字符串内容和末尾的空字符。
- 可以使用
strcpy
、strncpy
、strcat
等 C 库函数对 C 风格字符串进行操作。 - C 风格字符串容易发生缓冲区溢出和内存访问越界的问题,需要谨慎处理。
- 字符数组的大小是固定的,不能自动调整大小。
- C 风格字符串的长度可以使用
strlen
函数获取。
(2) string
字符串:
string
是 C++ 标准库中的一个类,提供了更安全、方便的字符串操作。- 可以使用字符串字面量、C 风格字符串、其他
string
对象等方式初始化string
对象。 string
对象能够自动管理内存,根据需要动态调整大小。string
类提供了丰富的成员函数,如size
、length
、append
、insert
、erase
、find
等,用于字符串的操作和处理。string
对象支持通过索引访问单个字符,如str[i]
。- 可以使用
+
运算符或append
函数将多个string
对象连接起来。 string
对象与 C 风格字符串之间可以方便地相互转换。
C 风格字符串的初始化: 使用字符串字面量初始化字符数组,如 char str[] = "Hello";
。
string
字符串的初始化:
- 使用字符串字面量初始化
string
对象,如string str = "Hello";
。 - 使用 C 风格字符串初始化
string
对象,如string str(cstr);
。 - 使用另一个
string
对象初始化新的string
对象,如string str2(str1);
。 - 使用字符和数量初始化
string
对象,如string str(5, 'A');
。
3.2 数组
参考文档:
- 数组 (C++) | Microsoft Learn
- C++ 数组_w3cschool
在 C++ 中,数组是一种存储相同类型元素的固定大小的顺序容器。
(1) 声明和定义数组:
- 声明数组的语法:
dataType arrayName[arraySize];
dataType
表示数组元素的类型,arrayName
是数组的名称,arraySize
是数组的大小。- 例如:
int arr[10];
声明了一个包含 10 个整数元素的数组arr
。
(2) 初始化数组:
- 数组可以在定义时进行初始化,使用大括号
{}
将初始值括起来。 - 例如:
int arr[5] = {1, 2, 3, 4, 5};
定义并初始化了一个包含 5 个整数的数组。 - 如果初始化时提供的元素数量少于数组大小,剩余的元素将被默认初始化为零。
- 如果初始化时没有指定数组大小,编译器会根据初始值的数量自动推断数组大小。
- 例如:
double arr[] = {1.1, 2.2, 3.3};
定义了一个包含 3 个双精度浮点数的数组。
(3) 访问数组元素:
-
数组元素可以通过数组名称和索引来访问。
-
数组的索引从 0 开始,最后一个元素的索引为数组大小减 1。
-
使用数组名称和方括号
[]
内的索引来访问特定的元素。 -
例如:
arr[0]
表示数组arr
的第一个元素,arr[4]
表示数组的第五个元素。 -
可以使用循环结构如
for
循环来遍历数组中的所有元素。for (int i = 0; i < arraySize; i++) { // 访问数组元素 arr[i] }
(4) 数组作为函数参数:
- 数组可以作为参数传递给函数。
- 当数组作为参数传递时,实际上传递的是数组的首地址。
- 在函数声明中,可以使用以下两种方式之一来声明数组参数:
void functionName(dataType arrayName[]);
void functionName(dataType* arrayName);
- 在函数内部,可以通过指针arithmetic来访问数组元素。
- 注意,函数无法确定数组的大小,需要额外传递数组大小或使用特定的终止条件。
(5) 数组与指针:
- 数组名称本身就是一个指向数组首元素的指针常量。
- 可以使用指针arithmetic来访问数组元素。
- 例如:
*(arr + i)
等同于arr[i]
,表示访问数组arr
的第i
个元素。
(6) 多维数组:
- C++ 支持多维数组,即数组的数组。
- 多维数组的声明语法:
dataType arrayName[size1][size2]...[sizeN];
- 例如:
int matrix[3][4];
声明了一个 3 行 4 列的二维整数数组。 - 访问多维数组元素:
arrayName[i][j]...[k]
。
需要注意的是,数组的大小是固定的,一旦声明就无法改变。如果需要动态调整数组大小,可以考虑使用 vector
或动态分配内存。
3.3 结构体与类
参考文档:struct (C++) | Microsoft Learn
结构体(Struct)是 C++ 中的一种用户自定义的复合数据类型,它允许将不同类型的数据元素组合成一个整体。
(1) 定义结构体:
-
使用
struct
关键字followed 结构体的名称来定义一个结构体。 -
在结构体定义中,列出所有成员变量及其类型。
struct Person { string name; int age; double height; };
(2) 声明结构体变量:
- 定义结构体后,可以创建该结构体类型的变量。
- 语法:
StructName variableName;
- 例如:
Person person;
声明了一个名为person
的Person
结构体变量。
(3) 访问结构体成员:
- 使用点运算符
.
来访问结构体变量的成员。 - 例如:
person.name
表示访问person
变量的name
成员。
(4) 初始化结构体:
- 可以在声明结构体变量时使用大括号
{}
对成员进行初始化。 - 例如:
Person person = {"John", 25, 1.75};
- 也可以逐个成员进行赋值,如
person.name = "John"; person.age = 25;
。
(5) 结构体数组:
- 可以创建结构体的数组,将多个结构体变量存储在一个数组中。
- 语法:
StructName arrayName[arraySize];
- 例如:
Person people[10];
声明了一个包含 10 个Person
结构体元素的数组。
(6) 结构体与函数:
- 结构体可以作为函数的参数和返回值。
- 当结构体作为参数传递给函数时,会创建一个结构体的副本。
- 如果结构体较大,可以考虑传递结构体的指针或引用,以避免复制开销。
(7) 结构体与指针:
- 可以使用指针指向结构体变量。
- 通过指针访问结构体成员时,使用箭头运算符
->
代替点运算符.
。 - 例如:
Person* ptr = &person; ptr->name = "John";
。
(8) 结构体与类的关系:
- 结构体和类在 C++ 中有许多相似之处,都用于封装数据和函数。
- 结构体默认情况下成员是公有(public)的,而类默认情况下成员是私有(private)的。
- 在现代 C++ 中,结构体主要用于封装纯数据,而类用于封装数据和行为。
- 结构体可以包含成员函数,但通常较少使用,而类通常同时包含数据成员和成员函数。
(9) 结构体的其他特性:
- 结构体可以嵌套,即一个结构体可以包含另一个结构体作为成员。
- 结构体可以包含位域,用于节省内存空间。
- 结构体可以继承,但通常不推荐使用结构体继承,而是使用类继承。
3.3 联合体
参考文档:union | Microsoft Learn
联合体(Union)是 C++ 中的一种特殊的数据类型,它允许在同一个内存位置存储不同类型的数据。联合体的所有成员共享相同的内存空间,因此同一时刻只能存储其中一个成员的值。
(1) 定义联合体:
-
使用
union
关键字followed 联合体的名称来定义一个联合体。 -
在联合体定义中,列出所有成员变量及其类型。
union Data { int intValue; double doubleValue; char charValue; };
(2) 声明联合体变量:
- 定义联合体后,可以创建该联合体类型的变量。
- 语法:
UnionName variableName;
- 例如:
Data data;
声明了一个名为data
的Data
联合体变量。
(3) 访问联合体成员:
- 使用点运算符
.
来访问联合体变量的成员。 - 例如:
data.intValue
表示访问data
变量的intValue
成员。 - 需要注意的是,尽管联合体可以存储不同类型的数据,但在某个时刻只能访问一个成员。
(4) 初始化联合体:
- 可以在声明联合体变量时对其进行初始化。
- 初始化时,只能为第一个成员赋值。
- 例如:
Data data = {10};
将intValue
成员初始化为 10。
(5) 联合体的大小:
- 联合体的大小等于其最大成员的大小。
- 编译器会根据最大成员的大小来分配联合体的内存空间。
- 例如,在上述
Data
联合体中,doubleValue
占用的内存最大,因此联合体的大小将与double
类型的大小相同。
(6) 联合体的用途:
- 联合体通常用于节省内存空间,特别是在有多个互斥的数据成员时。
- 联合体可以用于实现变体类型,根据某个标识来判断当前存储的数据类型。
- 联合体在与 C 语言的接口交互或处理二进制数据时也很有用。
(7) 联合体的限制:
- 联合体的成员不能有构造函数或析构函数。
- 联合体不能作为基类或派生类使用。
- 联合体不能包含引用类型的成员。
- 联合体的成员不能是类对象,除非该类有平凡的构造函数、析构函数和赋值运算符。
(8) 匿名联合体:
- 在 C++11 及更高版本中,支持匿名联合体。
- 匿名联合体没有名称,其成员可以直接访问,无需使用联合体变量名。
- 匿名联合体必须是非静态的、局部的或位于另一个联合体中。
4. 语句和表达式
4.1 常见表达式
参考文档:表达式 (C++) | Microsoft Learn
以下是 C++ 中常见的表达式类型:
表达式类型 | 描述 | 简单例子 |
---|---|---|
算术表达式 | 使用算术运算符(如 +, -, *, /, %)对数值进行运算的表达式 | a + b , x * y , (m - n) / k |
关系表达式 | 使用关系运算符(如 ==, !=, <, >, <=, >=)比较两个值的表达式,返回布尔值 | a == b , x > y , m <= n |
逻辑表达式 | 使用逻辑运算符(如 &&, ||, !)对布尔值进行运算的表达式 | a && b , x || y , !z |
赋值表达式 | 使用赋值运算符(如 =, +=, -=, *=, /=, %=)将值赋给变量的表达式 | a = 10 , x += 5 , y *= 2 |
位运算表达式 | 使用位运算符(如 &, |, ^, ~, <<, >>)对整数进行位级运算的表达式 | a & b , x | y , ~z , m << 2 , n >> 1 |
条件表达式 | 使用条件运算符( ? : )根据条件选择不同的值的表达式 | a > b ? x : y |
逗号表达式 | 使用逗号运算符(,)将多个表达式组合成一个表达式,从左到右依次求值 | a = 1, b = 2, c = 3 |
函数调用表达式 | 调用函数并传递参数的表达式 | sqrt(x) , max(a, b) , getValue() |
下标表达式 | 使用下标运算符([])访问数组或容器中特定位置的元素的表达式 | arr[i] , vec[j] , str[k] |
指针表达式 | 使用指针运算符(*, &)对指针进行操作的表达式 | *ptr , &var , ptr->member |
类型转换表达式 | 使用类型转换运算符(如 static_cast, dynamic_cast)显式地转换表达式的类型的表达式 | static_cast<int>(x) , dynamic_cast<Base*>(ptr) |
sizeof 表达式 | 使用 sizeof 运算符获取类型或表达式的字节大小的表达式 | sizeof(int) , sizeof(arr) , sizeof(a + b) |
类成员访问表达式 | 使用点运算符(.)或箭头运算符(->)访问类或结构体的成员的表达式 | obj.member , ptr->member |
new 表达式 | 使用 new 运算符动态分配内存并创建对象的表达式 | new int , new MyClass() , new int[10] |
delete 表达式 | 使用 delete 运算符释放动态分配的内存的表达式 | delete ptr , delete[] arr |
4.2 常见控制语句
参考文档:C++ 循环_w3cschool,语句 (C++) | Microsoft Learn
下面是 C++ 中常见的语句:
语句类型 | 描述 | 简单例子 |
---|---|---|
if 语句 | 根据条件执行不同的代码块 | if (condition) { ... } else { ... } |
switch 语句 | 根据表达式的值选择执行不同的 case 分支 | switch (expression) { case value1: ... break; case value2: ... break; default: ... } |
while 循环 | 当条件为真时重复执行代码块 | while (condition) { ... } |
do-while 循环 | 先执行一次代码块,然后在条件为真时重复执行 | do { ... } while (condition); |
for 循环 | 使用循环变量控制循环的执行 | for (initialization; condition; update) { ... } |
break 语句 | 跳出当前循环或 switch 语句 | while (...) { if (...) break; } |
continue 语句 | 跳过当前循环的剩余部分,进入下一次迭代 | for (...) { if (...) continue; ... } |
goto 语句 | 无条件跳转到指定的标签位置 | goto label; ... label: ... |
return 语句 | 从函数中返回值或控制权 | return value; 或 return; |
try-catch 语句 | 用于异常处理,捕获并处理异常 | try { ... } catch (ExceptionType& e) { ... } |
throw 语句 | 抛出异常 | throw exception; |
声明语句 | 声明变量、函数、类等 | int x; , void func(); , class MyClass { ... }; |
表达式语句 | 由表达式followed 分号构成的语句 | a = b + c; , func(); , ++i; |
复合语句(代码块) | 由一对大括号括起来的一组语句 | { int x = 1; y = x + 2; } |
空语句 | 只含有一个分号的语句,通常用作占位符 | ; |
每种语句都有其特定的语法和用途,通过组合这些语句,可以构建复杂的程序逻辑和控制流。
需要注意的是,goto
语句虽然存在,但在现代编程中应尽量避免使用,因为它可能导致代码的可读性和可维护性下降。
4.3 基本函数
参考文档:C++ 函数_w3cschool,函数 (C++) | Microsoft Learn
函数是 C++ 中重要的组成部分,用于将代码划分为可重用的模块。
(1) 函数声明:
- 函数声明指定函数的返回类型、名称和参数列表。
- 函数声明可以放在头文件中,以便其他文件引用。
- 语法:
returnType functionName(parameter1, parameter2, ...);
- 例如:
int add(int a, int b);
(2) 函数定义:
- 函数定义包含函数的实际实现代码。
- 函数定义可以放在源文件中。
- 语法:
returnType functionName(parameter1, parameter2, ...) { ... }
- 例如:
int add(int a, int b) { return a + b; }
(3) 函数使用:
- 通过函数名followed 括号中的参数列表来调用函数。
- 例如:
int result = add(3, 5);
(4) 参数传递:
- C++ 支持按值传递和按引用传递两种参数传递方式。
- 按值传递会复制参数的值,在函数内部对参数的修改不会影响原始值。
- 按引用传递将参数的引用传递给函数,在函数内部对参数的修改会影响原始值。
- 例如:
void func(int x, int& y);
(5) 默认参数:
- C++ 允许为函数参数指定默认值。
- 默认参数必须从右往左连续指定。
- 例如:
void func(int a, int b = 10, int c = 20);
(6) 可变参数:
- C++ 支持可变数量的参数,使用省略号
...
表示。 - 需要包含
<cstdarg>
头文件。 - 例如:
void func(int count, ...);
- 在函数内部,使用
va_list
、va_start
、va_arg
和va_end
宏来访问可变参数。
(7) 函数重载:
-
函数重载允许定义多个同名函数,但参数类型或数量不同。
-
编译器根据调用时提供的参数来决定调用哪个重载函数。
int add(int a, int b); double add(double a, double b);
(8) 内联函数:
- 内联函数使用
inline
关键字声明,建议编译器将函数调用替换为函数体的内容。 - 内联函数可以提高性能,但是过度使用可能导致代码膨胀。
- 例如:
inline int square(int x) { return x * x; }
(9) 函数指针:
- 函数指针是指向函数的指针。
- 函数指针可以作为参数传递给其他函数,实现回调机制。
- 例如:
int (*funcPtr)(int, int) = add;
(10) 递归函数:
- 递归函数是在函数内部调用自身的函数。
- 递归函数必须有一个终止条件,以避免无限递归。
- 例如:
int factorial(int n) { if (n <= 1) return 1; else return n * factorial(n - 1); }
4.4 模版函数
参考文档:函数模板 | Microsoft Learn。
C++ 模板函数是一种强大的工具,允许编写通用的、参数化的函数,可以处理不同类型的数据。模板函数通过将函数的参数类型和返回类型参数化,实现了代码的重用和灵活性。
(1) 函数模板定义,函数模板使用 template
关键字followed 尖括号中的模板参数列表来定义。模板参数可以是类型参数或非类型参数。函数模板的定义类似于普通函数,但是使用模板参数代替具体的类型。
template <typename T>
T add(T a, T b) {
return a + b;
}
(2) 函数模板实例化,当调用函数模板时,编译器会根据提供的实际参数类型自动生成对应的函数实例。这个过程称为函数模板的实例化。编译器会根据实际参数的类型来推导模板参数的类型,然后生成相应的函数定义。
int result1 = add(3, 5); // 实例化为 int add(int, int)
double result2 = add(2.5, 1.8); // 实例化为 double add(double, double)
(3) 显式模板参数指定,在某些情况下,编译器可能无法自动推导模板参数的类型,或者我们希望显式指定模板参数的类型。这时,可以使用显式模板参数指定,在函数名后面使用尖括号指定具体的模板参数类型。
int result = add<int>(3, 5);
(4) 函数模板重载,函数模板可以像普通函数一样进行重载。可以定义多个同名的函数模板,但是它们的模板参数列表或者函数参数列表必须不同。编译器会根据调用时提供的实际参数来选择最佳匹配的函数模板重载。
template <typename T>
T add(T a, T b) {
return a + b;
}
template <typename T>
T add(T a, T b, T c) {
return a + b + c;
}
(5) 函数模板特化,函数模板特化允许为特定的类型提供特定的实现,特化版本将替代通用的模板函数。特化函数模板时,需要指定具体的模板参数类型。
template <>
std::string add(std::string a, std::string b) {
return a + " " + b;
}
(6) 无关紧要转换,在函数模板重载解析过程中,编译器会考虑无关紧要转换。无关紧要转换是指不影响函数行为的转换,例如添加或删除 const、引用等。编译器会优先选择无需进行无关紧要转换的函数模板重载。
(7) 后置返回类型,C++11 引入了后置返回类型的语法,可以用于函数模板中。后置返回类型允许在函数参数列表之后指定返回类型,使用 auto
关键字followed 箭头 ->
和实际的返回类型。这对于返回类型依赖于模板参数的情况特别有用。
template <typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
return a * b;
}
C++ 模板函数提供了一种灵活而强大的方式来编写通用的、可重用的代码。通过函数模板,我们可以编写一次函数定义,并将其应用于多种类型。在设计和使用模板函数时,需要注意模板参数的选择、特化的必要性以及重载解析的规则,以确保代码的正确性和可读性。
4.5 常量函数
参考文档:C++ 常量表达式 | Microsoft Learn
C++11 引入了 constexpr
关键字,用于定义常量表达式函数和常量表达式变量。
(1) constexpr
函数:
-
constexpr
函数是一种特殊的函数,它的返回值可以在编译时计算出来。 -
constexpr
函数必须满足以下条件:- 函数体中只能包含一条
return
语句或者空语句。 - 函数体中不能有循环、分支等语句。
- 函数体中只能调用其他
constexpr
函数。 - 函数的参数和返回值必须是字面值类型或者
constexpr
变量。
- 函数体中只能包含一条
-
当
constexpr
函数的参数是常量表达式时,该函数会在编译时计算并返回结果。 -
当
constexpr
函数的参数不是常量表达式时,该函数会在运行时计算。constexpr int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); }
(2) constexpr
变量:
-
constexpr
变量是一种常量变量,它的值必须在编译时确定。 -
constexpr
变量必须使用常量表达式初始化。 -
一旦定义了
constexpr
变量,就不能再对其进行修改。constexpr int MAX_SIZE = 100; constexpr int array_size = MAX_SIZE + 10;
(3) constexpr
构造函数:
-
在 C++11 中,如果一个构造函数的函数体为空,并且所有成员变量都使用常量表达式初始化,那么这个构造函数可以被声明为
constexpr
构造函数。 -
constexpr
构造函数允许在编译时创建对象。class Point { public: constexpr Point(int x, int y) : x_(x), y_(y) {} private: int x_; int y_; };
(4) constexpr
和模板:
-
constexpr
函数可以与模板结合使用,以实现更加通用和灵活的编译时计算。 -
当模板参数是
constexpr
变量时,可以在编译时进行计算和优化。template <int N> constexpr int fibonacci() { return (N <= 1) ? N : fibonacci<N-1>() + fibonacci<N-2>(); }
使用 constexpr
函数和变量可以将某些计算从运行时转移到编译时,从而提高程序的性能。constexpr
还可以用于定义常量、数组大小、模板参数等,使得代码更加灵活和可维护。
5. 基本特性
5.1 内存模型
参考文档:
- C++ 动态内存_w3cschool
- new 运算符 (C++) | Microsoft Learn
- delete 运算符 (C++) | Microsoft Learn
C++ 的内存模型定义了程序如何管理和操作内存。在 C++ 中,内存分为几个主要的区域:栈(stack)、堆(heap)、全局/静态存储区和常量存储区。new
和 delete
运算符用于动态分配和释放堆内存。
(1) 栈(Stack):
- 栈是一块连续的内存区域,用于存储局部变量、函数参数和函数调用信息。
- 栈的内存分配和释放是自动进行的,遵循后进先出(LIFO)的原则。
- 当函数被调用时,函数的局部变量会被压入栈中,当函数返回时,这些变量会被自动释放。
- 栈的大小是有限的,分配的内存块在栈帧中连续存放。
(2) 堆(Heap):
- 堆是一块大的内存区域,用于动态分配内存。
- 堆的内存分配和释放需要显式地使用
new
和delete
运算符来管理。 - 堆内存的生命周期由程序员控制,需要手动释放已分配的内存以避免内存泄漏。
- 堆内存的分配和释放顺序是任意的,不像栈那样有严格的顺序。
(3) new
运算符:
-
new
运算符用于在堆上动态分配内存。 -
语法:
pointer = new type;
或pointer = new type[size];
-
new
运算符返回一个指向所分配内存的指针。 -
如果内存分配失败,
new
运算符会抛出std::bad_alloc
异常。int* ptr = new int; // 分配单个整数的内存 int* arr = new int[10]; // 分配整数数组的内存
(4) delete
运算符:
-
delete
运算符用于释放由new
运算符分配的内存。 -
语法:
delete pointer;
或delete[] pointer;
-
对于单个对象,使用
delete
; 对于数组,使用delete[]
。 -
在释放内存之前,需要确保指针指向有效的堆内存。
-
释放空指针是安全的,不会产生任何效果。
-
多次释放同一块内存会导致未定义行为。
delete ptr; // 释放单个对象的内存 delete[] arr; // 释放数组的内存
(5) 内存泄漏:
- 内存泄漏是指动态分配的内存没有被正确释放,导致内存资源被浪费。
- 为了避免内存泄漏,需要确保每个
new
对应一个delete
,并在不再需要内存时及时释放。 - 智能指针(如
std::unique_ptr
和std::shared_ptr
)可以帮助管理动态分配的内存,减少手动内存管理的错误。
(6) 内存分配失败:
- 当使用
new
运算符分配内存时,如果系统无法满足内存请求,会抛出std::bad_alloc
异常。 - 可以使用
try-catch
语句捕获该异常并进行相应的错误处理。 - 也可以使用
new (std::nothrow)
形式的new
运算符,它在分配失败时返回空指针而不抛出异常。
(7) 定位 new
运算符:
- 定位
new
运算符允许在已分配的内存上构造对象。 - 语法:
new (address) type;
- 定位
new
运算符不分配内存,而是在指定的内存地址上构造对象。 - 在使用定位
new
时,需要确保提供的内存地址有足够的空间来容纳对象。
(8) 重载 new
和 delete
运算符:
- C++ 允许重载全局的
new
和delete
运算符,以自定义内存分配和释放的行为。 - 也可以为特定的类重载
new
和delete
运算符,以实现类特定的内存管理策略。 - 重载
new
和delete
运算符时,需要遵循一定的规则和约定,以确保与标准库的兼容性。
手动管理内存也带来了一些挑战,如内存泄漏和悬空指针等问题。为了简化内存管理并避免常见的陷阱,现代 C++ 引入了智能指针和资源管理的概念(如 RAII)。
5.2 存储和链接
参考文档:
- 翻译单元和链接 (C++) | Microsoft Learn
- 普通、标准布局、POD 和文本类型 | Microsoft Learn
C++ 中的存储类别和链接性是两个重要的概念,它们决定了变量和函数的生命周期、可见性和链接方式。
存储类别定义了变量的生命周期和可见性。C++ 中有以下几种存储类别:
(1) 自动存储类别(automatic storage class):
-
自动存储类别适用于局部变量,包括函数参数和函数内部定义的变量。
-
自动变量在函数被调用时分配内存,在函数返回时自动销毁。
-
自动变量的生命周期与函数的生命周期相同。
-
自动变量的可见性仅限于函数内部。
void func() { int x; // 自动变量 // ... }
(2) 静态存储类别(static storage class):
-
静态存储类别适用于静态局部变量和静态全局变量。
-
静态变量在程序启动时分配内存,在程序终止时销毁。
-
静态局部变量的生命周期与程序的生命周期相同,但可见性仅限于函数内部。
-
静态全局变量的生命周期与程序的生命周期相同,可见性取决于链接性。
void func() { static int count = 0; // 静态局部变量 // ... } static int global_var; // 静态全局变量
(3) 动态存储类别(dynamic storage class):
-
动态存储类别适用于使用
new
运算符动态分配的内存。 -
动态内存的生命周期由程序员控制,需要使用
delete
运算符显式释放。 -
动态内存的可见性取决于指向它的指针的作用域。
int* ptr = new int; // 动态分配内存 // ... delete ptr; // 释放内存
(4) 线程存储类别(thread storage class):
-
线程存储类别适用于线程局部变量,使用
thread_local
关键字声明。 -
线程局部变量在每个线程中都有独立的副本,生命周期与线程的生命周期相同。
-
线程局部变量的可见性仅限于声明它的线程。
thread_local int thread_var; // 线程局部变量
链接性决定了变量和函数在不同翻译单元之间的可见性和链接方式。C++ 中有以下几种链接性:
(1) 外部链接性(external linkage):
-
外部链接性适用于在文件范围内声明的非静态函数和非静态全局变量。
-
具有外部链接性的实体可以在多个翻译单元之间共享和访问。
-
在一个翻译单元中定义,在其他翻译单元中声明(使用
extern
关键字)即可使用。// file1.cpp int global_var = 10; // 外部链接性 // file2.cpp extern int global_var; // 声明外部链接性变量
(2) 内部链接性(internal linkage):
-
内部链接性适用于静态全局变量和静态函数。
-
具有内部链接性的实体只能在当前翻译单元内部访问。
-
在不同的翻译单元中,可以定义同名的内部链接性实体,它们互不干扰。
static int internal_var; // 内部链接性变量 static void internal_func() { // 内部链接性函数 // ... }
(3) 无链接性(no linkage):
-
无链接性适用于局部变量、函数参数和类的成员变量。
-
无链接性实体只能在当前作用域内访问,不能在其他作用域或翻译单元中使用。
void func() { int local_var; // 无链接性变量 // ... }
如果要强制一个全局名称具有内部链接,可以将它显式声明为 static
。 此关键字将它的可见性限制在声明它的同一翻译单元内。 在此上下文中,static
表示与应用于局部变量时不同的内容。默认情况下,以下对象具有内部链接:
- **
const
**对象 - **
constexpr
**对象 - **
typedef
**对象 - 命名空间范围中的
static
对象
若要为 const
对象提供外部链接,请将其声明为 extern
并为其赋值:
extern const int value = 42;
5.4 名称空间
参考文档:
C++ 命名空间_w3cschool
命名空间 (C++) | Microsoft Learn
C++ 中的命名空间(namespace)是一种将相关的标识符(如变量、函数、类等)组织在一起的机制,以避免命名冲突并提高代码的可读性和可维护性。命名空间提供了一种逻辑上的分组和封装,使得不同库或模块之间的标识符可以共存而不会相互干扰。
(1) 命名空间使用 namespace
关键字followed 命名空间的名称来定义。命名空间的定义可以跨越多个文件,可以嵌套,也可以是匿名的。
-
基本语法:
namespace namespace_name { // 声明和定义 }
-
跨文件的命名空间定义:
// file1.h namespace my_namespace { void func1(); } // file1.cpp namespace my_namespace { void func1() { // ... } }
-
嵌套的命名空间:
namespace outer_namespace { namespace inner_namespace { void func(); } }
-
匿名命名空间:
namespace { void func(); }
匿名命名空间中的标识符具有内部链接性,只能在当前翻译单元内部访问。
(2) 要访问命名空间中的标识符,可以使用以下几种方式:
-
完全限定名:
// namespace_name::identifier my_namespace::func1();
-
using
声明:using namespace_name::identifier;
using
声明将特定的标识符引入当前作用域,可以直接使用标识符而不需要完全限定名。using my_namespace::func1; func1();
-
using
指令:using namespace namespace_name;
using
指令将整个命名空间引入当前作用域,可以直接使用命名空间中的所有标识符。using namespace my_namespace; func1();
(3) 命名空间的其他特性
-
命名空间别名,可以使用
namespace
关键字为命名空间创建别名,以简化长命名空间的使用。//namespace alias_name = namespace_name; namespace mn = my_namespace; mn::func1();
-
内联命名空间(C++11),内联命名空间使用
inline
关键字定义,其中的标识符会被自动导出到父命名空间中。namespace my_namespace { inline namespace inner { void func(); } }
可以直接使用
my_namespace::func()
访问内联命名空间中的函数。 -
未命名的命名空间(C++11),未命名的命名空间是一种特殊的匿名命名空间,它的名称由编译器自动生成。
namespace { void func(); }
未命名的命名空间在每个翻译单元中都是唯一的,相当于静态链接。
(4) 标准库命名空间,C++ 标准库中的所有标识符都被定义在一个名为 std
的命名空间中。要使用标准库中的标识符,可以使用完全限定名(std::identifier
)或者使用 using
声明/指令。
#include <iostream>
// 使用完全限定名
std::cout << "Hello, World!" << std::endl;
// 使用 using 声明
using std::cout;
using std::endl;
cout << "Hello, World!" << endl;
// 使用 using 指令
using namespace std;
cout << "Hello, World!" << endl;
在设计和实现 C++ 程序时,应该合理利用命名空间,遵循一致的命名约定,并根据需要选择适当的命名空间使用方式,以确保代码的清晰性和模块化。
6. 基础总结
本文主要基于W3Cschool和微软学习文档对C++基础语法做了一个简单的介绍,C++语法十分繁杂,入门书籍基本都是千页以上,因此学习时基本不可能一次看书学会,必须基于开源库源码分析和基础编程题进行练手。
对于初学者,W3Cschool教程是一个非常不错的学习途径,有充足的示例文档和练手代码;对于日常工作学习和参考,微软学习文档则更加合适,如果后续进一步专精,则需要接触更加深入的C++书籍。
这篇文章只是对C++浅尝,后续还会结合源码分析持续学习和总结C++细节处理部分,重要不是其语法规则,而是隐藏在语法背后的编程实践原理,这才是最宝贵的财富。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~