C++编程:从基础到类的深入解析
立即解锁
发布时间: 2025-08-16 01:20:55 阅读量: 3 订阅数: 29 


C++编程语言第四版:核心概念与技术
# C++编程:从基础到类的深入解析
## 1. 头文件与包含保护
在C++编程中,头文件是组织代码的重要工具。为了避免头文件被重复包含,传统的做法是使用包含保护(include guards)。例如:
```cpp
// error.h:
#ifndef CALC_ERROR_H
#define CALC_ERROR_H
namespace Error {
// ...
}
#endif
// CALC_ERROR_H
```
当`CALC_ERROR_H`被定义时,编译器会忽略`#ifndef`和`#endif`之间的内容。这样,在编译过程中,头文件`error.h`第一次被包含时,其内容会被读取,并且`CALC_ERROR_H`会被赋值。如果在编译过程中再次遇到`error.h`,其内容将被忽略。
由于头文件可能在任意上下文中被包含,且没有命名空间保护来防止宏名称冲突,因此建议为包含保护使用较长且复杂的名称。同时,虽然C++实现会优化头文件的处理,但过度包含头文件可能会导致编译时间过长,并且会引入大量的声明和宏,这可能会以不可预测的方式影响程序的含义。所以,头文件应该仅在必要时包含。
## 2. 程序结构与`main`函数
一个程序是由多个单独编译的单元通过链接器组合而成的。在这个集合中使用的每个函数、对象、类型等都必须有唯一的定义。程序必须包含一个名为`main()`的函数,程序的主要计算从全局函数`main()`的调用开始,并在`main()`返回时结束。
`main()`的返回类型是`int`,所有实现都支持以下两种版本的`main()`:
```cpp
int main() { /* ... */ }
int main(int argc, char* argv[]) { /* ... */ }
```
一个程序只能提供这两种版本中的一种。此外,实现可以允许其他版本的`main()`。`argc`和`argv`版本用于从程序的环境中传递参数。`main()`返回的`int`值会作为程序的结果传递给调用`main()`的系统,非零返回值表示程序出现错误。
## 3. 非局部变量的初始化
### 3.1 初始化规则
原则上,定义在任何函数外部的变量(即全局、命名空间和类静态变量)在`main()`被调用之前会被初始化。在一个翻译单元中,这些非局部变量会按照它们的定义顺序进行初始化。如果一个变量没有显式的初始化器,它会被默认初始化为其类型的默认值。对于内置类型和枚举类型,默认初始值是0。例如:
```cpp
double x = 2;
// nonlocal variables
double y;
double sqx = sqrt(x+y);
```
在这个例子中,`x`和`y`会在`sqx`之前被初始化,因此会调用`sqrt(2)`。
### 3.2 不同翻译单元中全局变量的初始化顺序
不同翻译单元中全局变量的初始化顺序没有保证。因此,在不同编译单元的全局变量初始化器之间创建顺序依赖是不明智的。此外,无法捕获全局变量初始化器抛出的异常。通常,最好尽量减少全局变量的使用,特别是要限制需要复杂初始化的全局变量的使用。
### 3.3 替代全局变量的方法
一个返回引用的函数通常是全局变量的一个很好的替代方案。例如:
```cpp
int& use_count()
{
static int uc = 0;
return uc;
}
```
调用`use_count()`现在的行为就像一个全局变量,只是它在第一次使用时才会被初始化。例如:
```cpp
void f()
{
cout << ++use_count();
// read and increment
// ...
}
```
然而,这种技术不是线程安全的。虽然局部静态变量的初始化是线程安全的,但`++`操作可能会导致数据竞争。
## 4. 初始化与并发
考虑以下代码:
```cpp
int x = 3;
int y = sqrt(++x);
```
`x`和`y`可能的值是多少?直观的答案是“3和2”。原因是使用常量表达式初始化静态分配的对象是在链接时完成的,所以`x`变成3。然而,`y`的初始化器不是常量表达式(`sqrt()`不是`constexpr`),所以`y`直到运行时才会被初始化。在单个翻译单元中,静态分配对象的初始化顺序是明确定义的,它们会按照定义顺序进行初始化,所以`y`变成2。
但是,如果使用多个线程,每个线程都会进行运行时初始化,并且没有隐式提供互斥来防止数据竞争。因此,一个线程中的`sqrt(++x)`可能在另一个线程成功递增`x`之前或之后发生,所以`y`的值可能是`sqrt(4)`或`sqrt(5)`。
为了避免这些问题,应该:
- 尽量减少静态分配对象的使用,并保持它们的初始化尽可能简单。
- 避免依赖其他翻译单元中动态初始化的对象。
此外,为了避免初始化中的数据竞争,可以按以下顺序尝试这些技术:
1. 使用常量表达式进行初始化(注意,没有初始化器的内置类型会被初始化为零,标准容器和字符串会在链接时初始化为空)。
2. 使用没有副作用的表达式进行初始化。
3. 在已知的单线程“启动阶段”进行初始化。
4. 使用某种形式的互斥。
## 5. 程序终止
程序可以通过以下几种方式终止:
1. 从`main()`返回。
2. 调用`exit()`。
3. 调用`abort()`。
4. 抛出未捕获的异常。
5. 违反`noexcept`。
6. 调用`quick_exit()`。
此外,还有各种行为不当和依赖于实现的方式会导致程序崩溃(例如,将一个`double`除以零)。
如果使用标准库函数`exit()`终止程序,已构造的静态对象的析构函数会被调用。然而,如果使用标准库函数`abort()`终止程序,它们不会被调用。需要注意的是,这意味着`exit()`不会立即终止程序。在析构函数中调用`exit()`可能会导致无限递归。`exit()`的类型是:
```cpp
void exit(int);
```
与`main()`的返回值一样,`exit()`的参数会作为程序的值返回给“系统”,零表示程序成功完成。
调用`exit()`意味着调用函数及其调用者的局部变量的析构函数不会被调用。抛出异常并捕获它可以确保局部对象被正确销毁。此外,调用`exit()`会终止程序,而不给调用`exit()`的函数的调用者处理问题的机会。因此,通常最好通过抛出异常来离开一个上下文,并让处理程序决定下一步该做什么。
C(和C++)标准库函数`atexit()`提供了在程序终止时执行代码的可能性。例如:
```cpp
void my_cleanup();
void somewhere()
{
if (atexit(&my_cleanup)==0) {
// my_cleanup will be called at normal termination
}
else {
// oops: too many atexit functions
}
}
```
这与程序终止时全局变量析构函数的自动调用非常相似。`atexit()`的参数不能接受参数或返回结果,并且`atexit`函数的数量有实现定义的限制。`atexit()`返回非零值表示达到了限制。
`quick_exit()`函数与`exit()`类似,只是它不会调用任何析构函数。可以使用`at_quick_exit()`注册要由`quick_exit()`调用的函数。`exit()`、`abort()`、`quick_exit()`、`atexit()`和`at_quick_exit()`函数在`<cstdlib>`中声明。
## 6. 编程建议
为了编写高质量的C++代码,以下是一些建议:
1. 使用头文件来表示接口并强调逻辑结构。
2. 在实现其函数的源文件中包含头文件。
3. 不要在不同的翻译单元中定义具有相同名称但含义相似但不同的全局实体。
4. 避免在头文件中定义非内联函数。
5. 仅在全局作用域和命名空间中使用`#include`。
6. 仅包含完整的声明。
7. 使用包含保护。
8. 在命名空间中包含C头文件以避免全局名称。
9. 使头文件自包含。
10. 区分用户接口和实现者接口。
11. 区分普通用户接口和专家用户接口。
12. 在打算作为非C++程序的一部分执行的C++代码中,避免使用需要运行时初始化的非局部对象。
## 7. C++类的基础
### 7.1 类的概述
C++类是创建新类型的工具,这些新类型可以像内置类型一样方便地使用。类是用户定义的类型,由一组成员组成,最常见的成员类型是数据成员和成员函数。成员函数可以定义对象的初始化、复制、移动和清理(销毁)的含义。成员可以使用`.`(点)运算符访问对象,使用`->`(箭头)运算符访问指针。可以为类定义运算符,如`+`、`!`和`[]`。类是一个包含其成员的命名空间,公共成员提供类的接口,私有成员提供实现细节。`struct`是一种成员默认是公共的类。
例如:
```cpp
class X {
private:
// the representation (implementation) is private
int m;
public:
// the user interface is public
X(int i =0) :m{i} { }
// a constructor (initialize the data member m)
int mf(int i)
// a member function
{
int old = m;
m = i;
// set a new value
return old;
// return the old value
}
};
X var {7}; // a variable of type X, initialized to 7
int user(X var, X* ptr)
{
int x = var.mf(7);
// access using . (dot)
int y = ptr->mf(9);
// access using -> (arrow)
int z = var.m;
// error : cannot access private member
}
```
### 7.2 成员函数
可以使用`struct`来定义`Date`的表示,并使用一组函数来操作这种类型的变量。例如:
```cpp
struct Date {
// representation
int d, m, y;
};
void init_date(Date& d, int, int, int);
// initialize d
void add_year(Date& d, int n);
// add n years to d
void add_month(Date& d, int n);
// add n months to d
void add_day(Date& d, int n);
// add n days to d
```
为了建立数据类型`Date`和这些函数之间的显式连接,可以将这些函数声明为成员函数:
```cpp
struct Date {
int d, m, y;
void init(int dd, int mm, int yy);
// initialize
void add_year(int n);
// add n years
void add_month(int n);
// add n months
void add_day(int n);
// add n days
};
```
成员函数只能通过标准的结构成员访问语法为特定类型的变量调用。例如:
```cpp
Date my_birthday;
void f()
{
Date today;
today.init(16,10,1996);
my_birthday.init(30,12,1950);
Date tomorrow = today;
tomorrow.add_day(1);
// ...
}
```
在定义成员函数时,必须指定结构名称。在成员函数中,成员名称
0
0
复制全文
相关推荐










