引言
在专栏C++教程的第二篇C++配置开发环境与使用中,我们顺利完成了从搭建C++开发环境到运行首个简单程序的所有步骤。 在专栏C++教程的第三篇中,将理解C++程序的基本结构和核心语法,对C++程序的构建块进行深度解析,并通过实例演示如何构造一个简单的C++程序。下一篇点此跳转
本篇不带有目录,查看目录请看目录栏→
C++程序的基本结构
-
源文件与编译单位
在深入探讨C++编程时,理解其程序的基本结构至关重要。C++源代码通常由多个.cpp
源文件组成,这些源文件被独立编译为对象文件,然后通过链接器将所有相关的对象文件组合成一个可执行文件。每个源文件都作为一个单独的编译单位,包含了变量定义、函数声明和函数定义等元素。
全局作用域内的main()
函数是整个C++程序的入口点。它具有特定的签名,即返回类型为int
,表示程序退出时的状态码,其中0通常代表正常退出,非零值则可能表示异常或错误情况。例如:
#include <iostream> // 引入标准库中的<iostream>头文件,提供cout用于输出功能
using namespace std;
/* 引入命名空间std中的所有成员,
* 使得我们可以直接使用std命名空间中的函数、类、变量等,而不
* 需要在每个标识符前面加上std::前缀。
*/
int main() {
cout << "Hello, World!\n"; // 输出“Hello, World!”到标准输出(通常是控制台)
// 一般情况语句是以分号(;)结尾的
return 0; // 表示程序正常结束并返回状态码0
}
预处理指令
预处理指令是一种在源代码被编译器真正解析和编译之前由预处理器执行的特殊命令。这些指令允许开发者进行文本替换、条件编译、文件包含等操作,对提高代码的模块化程度、适应不同环境编译以及优化编译过程具有重要意义。
宏定义(#define
)
-
#define
用于创建符号常量或简单的函数式宏。
#define PI 3.14159265358979323846 // 定义一个常量宏
#define SQUARE(x) ((x) * (x)) // 定义一个函数式宏
注意:使用宏时应谨慎,避免引入副作用和未预期的行为。C++11及以后版本推荐使用const
变量或constexpr代替简单的常量宏。
条件编译(#define
, #ifndef, #else, #endif)
-
这些指令可以实现基于预定义宏或其他自定义宏来决定哪些代码块会被编译器看到。
#ifdef DEBUG_MODE
cout << "Debugging information..." << endl; // 在DEBUG_MODE被定义时编译这部分代码
#endif
#ifndef NDEBUG
// 在NDEBUG未定义(即调试模式下)时编译此部分代码
assert(condition); // 断言检查
#endif
文件包含(#include)
-
用于插入其他源文件或头文件的内容到当前文件中。
#include <iostream> // 引入标准库头文件
#include "my_header.h" // 引入自定义头文件
取消宏定义(#undef)
-
删除先前定义过的宏。
#define MY_MACRO ...
...
#undef MY_MACRO // 取消MY_MACRO的定义
行号与文件信息(#line)
-
更改编译器跟踪的当前行号和文件名信息,主要用于调试和错误报告。
#line 100 "custom_filename.cpp"
编译指示(#pragma)
-
pragma是编译器特定的指令,允许程序员向编译器提供附加的信息或请求特定的编译器行为。例如,控制警告级别、开启/关闭某些特性等。
预处理指令的应用场景
-
条件编译以支持多平台开发:通过宏定义区分不同的操作系统、硬件架构等,编写针对特定平台的代码段。
-
代码模块化:通过头文件(
.h
或.hpp
)封装类、结构体和函数声明,使代码更易于维护和复用。 -
编译时选项:根据编译时传递的标志决定是否启用特定功能,如日志输出、性能分析、内存检查等。
-
调试辅助:利用条件编译和宏定义实现只在调试版本中生效的功能,比如打印调试信息、开启额外的检查机制等。
-
注释
在C++中,有两种类型的注释用于增强代码的可读性和维护性:
-
单行注释:以两个斜线
//
开始,直到该行结束都是注释内容。
// 这是一个单行注释,不会被编译器解析
-
多行注释:用一对
/*
和*/
包围起来的任何文本都被视为注释,可以跨越多行。
/*
* 这是一个多行注释,
* 可以包含多行文字描述,
* 并且不会影响编译结果。
*/
C++基本语法元素
变量与数据类型
在C++中,变量是用来存储数据的内存位置,每个变量都有一个特定的数据类型,决定了它能存储何种类型的值。以下列举了部分标准数据类型:
-
整型(Integral Types):
包括int
(通常为32位)、short int
(短整型,长度较短,至少16位)、long int
(长整型,至少32位)和long long int
(扩展长整型,至少64位),用于存储整数值。
-
浮点数类型(Floating-Point Types):
主要有float
(单精度浮点数,通常为32位,精度较低)、double
(双精度浮点数,通常是64位,精度较高)和long double
(扩展精度浮点数,平台相关,但通常比double
拥有更高的精度)。这些类型用来存储带有小数部分的数值。
-
字符类型(Character Type):
char
类型可以存储一个ASCII字符或ISO Latin-1编码中的字符,占用一字节(8位)。
-
布尔类型(Boolean Type):
bool
类型只能取两个可能的值,即true
和false
,常用于条件判断和逻辑运算。
-
指针类型(Pointer Type):
指针是一种特殊的变量,它存储的是其他变量的内存地址,而不是实际的值。例如声明int* p;
表示p是一个指向整型变量的指针。
-
引用类型(Reference Type):
引用提供了一种对已存在对象的别名机制,一旦引用被初始化后,就不能改变其引用的对象。如int& ref = value;
意味着ref是value的一个引用,任何对ref的操作都会直接作用于value上。
声明与初始化
在C++中,变量在声明时即可进行初始化,这种方式确保了变量在使用前就有了确定的初始值,提高了代码的清晰性和安全性。下面是一些示例:
// 声明并初始化整型变量age
int age = 25;
// 初始化双精度浮点数变量pi
double pi = 3.14159;
// 初始化字符变量c
char c = 's';
此外,C++还提供了多种存储区域以管理不同生命周期的变量:
-
静态存储区(Static Storage Duration):
全局变量和静态局部变量存放在此区域,它们在程序开始执行之前分配空间,并在程序结束时释放。例如:
static int globalVar = 10; // 全局变量
void func() {
static int localVar = 20; // 静态局部变量
}
-
堆栈区(Stack):
函数内部声明的非静态局部变量存放在栈区内,随着函数调用的进入和退出自动分配和释放空间。如上述例子中的age
和pi
。
-
动态内存分配(Dynamic Allocation):
通过new
关键字在自由存储区(heap)中动态地分配内存空间,需要程序员手动调用delete
来释放。例如:
int* dynamicVar = new int(30); // 动态分配一个整型变量
delete dynamicVar; // 使用完毕后释放内存
运算符与表达式
算术运算符
算术运算符是进行基本数学计算的基础工具。具体包括:
-
加法(+):用于数值的相加,例如
int sum = a + b;
会将变量a和b的值相加并赋给sum。
-
减法(-):用于数值的相减,如
int difference = a - b;
将得到a和b的差值。
-
乘法(*):用于数值的相乘,如
double product = a * b;
计算a和b的乘积。
-
除法(/):用于数值的相除,如
float quotient = a / b;
计算a除以b的结果。需要注意的是,整数除法可能导致精度丢失,而浮点数除法则可以保留小数部分。
-
模运算符(%):也称为取余运算符,返回两个整数相除后的余数,如
int remainder = a % b;
得到a除以b的余数。
关系运算符
关系运算符用于比较两个操作数之间的大小或等价关系,并返回一个布尔值(true或false)。这些运算符包括:
- 等于(==):判断两个操作数是否相等,如
if (x == y) {...}
检查x和y是否具有相同的值。
- 不等于(!=):判断两个操作数是否不相等,如
if (x != y) {...}
当x和y的值不同时执行代码块。
- 小于(<):判断左操作数是否小于右操作数,如
if (a < b) {...}
当a小于b时执行条件语句内的内容。
- 大于(>):判断左操作数是否大于右操作数,如
if (c > d) {...}
在c大于d的情况下执行相应操作。
- 小于等于(<=):判断左操作数是否小于或等于右操作数,如
if (e <= f) {...}
当e不大于f时执行代码块。
- 大于等于(>=):判断左操作数是否大于或等于右操作数,如
if (g >= h) {...}
当g不小于h时执行相关指令。
- 逻辑与(&&):当两边的操作数都为真时结果才为真,否则为假。如
if (condition1 && condition2) {...}
表示只有当condition1和condition2同时为真时才会执行代码块。
- 逻辑或(||):只要两边有一个操作数为真,结果就为真;只有两边都为假时结果才为假。例如
if (expression1 || expression2) {...}
表示expression1或expression2中任意一个为真都会执行相应的代码。
- 逻辑非(!):对单个布尔表达式取反,即如果表达式的值为真,则逻辑非后的结果为假;若表达式的值为假,则结果为真。如
if (!isTrue) {...}
当isTrue为假时执行代码块。
- 基本赋值(=):直接将右侧表达式的值赋予左侧变量,例如
int x; x = 10;
为变量x赋值为10。
复合赋值运算符:
赋值运算符不仅包括基本的等号(=),还有复合赋值运算符:
- 递增(+=):将操作数的值增加指定的值后重新赋给它自身,如
x += 5;
等效于x = x + 5;
- 递减(-=):将操作数的值减少指定的值后重新赋给它自身,如
y -= 3;
等效于y = y - 3;
- 乘以并赋值(*=):将操作数与指定值相乘后赋给它自身,如
z *= 2;
相当于z = z * 2;
- 除以并赋值(/=):将操作数除以指定值后赋给它自身,如
w /= 4;
相当于w = w / 4;
- 取模并赋值(%=):将操作数与指定值求模后赋给它自身,如
m %= 7;
等同于m = m %
7;
自增自减运算符
自增和自减运算符有两种形式:前置(++x、--x)和后置(x++, x--)。
- 前置自增(++x):先将变量x的值加1,然后使用新的值参与后续表达式计算。
- 后置自增(x++):先使用变量x当前的值参与表达式计算,然后将x的值加1。
- 前置自减(--x):先将变量x的值减1,然后使用新的值参与后续表达式计算。
- 后置自减(x--):先使用变量x当前的值参与表达式计算,然后将x的值减1。
控制流语句
控制流语句是程序设计中的关键构造块,它们允许根据不同的条件和循环逻辑来控制代码的执行路径。在C++中,有多种类型的条件判断语句和循环结构,它们构成了程序流程的基础。
条件语句:
条件语句通常指程序运行时遇到条件判断,根据条件的结果选择执行不同代码块的机制。主要分为两种基本形式:
-
if语句:最简单的分支结构,当给定的条件为真(或满足特定逻辑)时,程序会执行对应的代码块。
if (condition) {
// 当条件满足时执行这里的代码
}
-
if-else语句:在if的基础上增加一个备用分支,用于处理条件不满足的情况。
if (condition) {
// 当条件满足时执行这里的代码
} else {
// 当条件不满足时执行这里的代码
}
-
嵌套if-else结构:为处理更复杂的多条件情况,可将if和else语句嵌套使用,形成多个层级的判断。
if (condition1) {
// 条件1满足时的逻辑
} else if (condition2) {
// 条件1不满足但条件2满足时的逻辑
} else {
// 所有条件都不满足时的逻辑
}
-
switch-case语句:除了上述基于布尔表达式的分支结构外,C++还提供了另一种分支结构——switch-case语句,主要用于处理多分支的选择,且每个分支通常是基于某个变量的等值比较。
int choice;
cin >> choice;
switch (choice) {
case 1:
// 用户选择1时的逻辑
break;
case 2:
// 用户选择2时的逻辑
break;
default:
// 用户选择其他值时的逻辑
}
循环语句:
循环语句用于重复执行一段代码,直到满足某个终止条件为止。
-
while循环: while循环首先检查给定的条件,如果条件为真,则执行循环体内的代码。下面是一个简单的while循环实例:
int count = 0;
while (count < 10) { // 循环条件为count小于10
// 在这里执行循环体内的操作
++count; // 自增操作使count递增更新
cout << count << "\n"; // 输出当前计数值
}
-
for循环: for循环是一种更紧凑的循环形式,它包含了初始化、条件测试和迭代更新三个部分,尤其适合于已知循环次数的情况。
for (int i = 0; i < 10; ++i) { // 初始化(i=0)、条件(i<10)和更新操作(++i)
cout << i << "\n"; // 输出当前循环变量i的值
}
-
do...while循环: do...while循环与while循环的主要区别在于其先执行一次循环体,然后才检查条件。这意味着do...while至少会执行一次循环体。
int j = 0;
do {
cout << j << "\n"; // 先执行输出操作
j++; // 然后更新增加j的值
} while (j < 10); // 再检查条件,如果j仍小于10,则继续循环
break
和continue
在C++编程中,循环结构是实现重复执行代码逻辑的核心工具。而为了让循环的控制更加灵活精准,C++提供了两种特殊的流程控制语句——break
和continue
。本文将深入剖析这两种语句的作用、使用场景以及相应的最佳实践。
-
break语句
break
语句用于提前终止当前所在的最内层循环(包括for、while、do-while或switch语句)一旦遇到break
,程序将立即跳出该循环,并继续执行循环体后面的语句。
例如,在遍历数组查找特定元素时,一旦找到就无须再继续遍历:
int arr[] = {1, 2, 3, 4, 5};
int target = 3;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) {
if (arr[i] == target) {
cout << "Element found at index: " << i << endl;
break; // 找到目标值后立刻结束循环
}
}
-
continue语句
相比之下,continue
语句并不退出整个循环,而是跳过当前循环体中剩余的语句,直接进入下一次循环迭代。
例如,在打印数组的所有非零元素时,可以利用continue
避免输出零值:
int arr[] = {0, 1, 0, 2, 0, 3};
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) {
if (arr[i] == 0) {
continue; // 跳过当前循环迭代,不处理零值
}
cout << "Non-zero element at index: " << i << ", value: " << arr[i] << endl;
}
注意事项
-
明确意图:在使用
break
和continue
时,确保你的意图清晰明了,避免让代码难以理解和维护。在条件满足时使用这些语句,能够提高代码的可读性和效率。 -
避免滥用:过度依赖
break
和continue
可能会使代码结构变得复杂且不易于调试。在可能的情况下,优先考虑使用逻辑判断而非强制中断循环。 -
嵌套循环中的使用:在多层嵌套循环中,
break
只会终止最内层的循环,若需要终止外层循环,通常需要设置额外的布尔标志变量。 -
switch语句中的break:在
switch
语句中,每个case
块末尾的break
是常见的做法,用来防止“穿透”到下一个case
。如果故意省略break
,则可以实现多个case
共享相同的执行语句。
函数
函数是C++中的基本模块化单元,用于封装特定任务的代码段。包括函数声明、函数定义、形参列表、返回类型及函数体。同时,C++还支持内联函数、默认参数、函数重载等高级特性。
函数声明与定义
函数的声明(Function Declaration)通常在函数调用之前出现,它向编译器提供函数的接口信息,包括函数名、返回类型以及形参列表(参数类型及其顺序)。例如:
// 函数声明
int add(int a, int b); // 声明了一个接受两个整数并返回一个整数的add函数
// 函数定义
int add(int a, int b) {
return a + b; // 函数体,具体实现了加法操作
}
形参列表与返回类型
形参列表指定了函数接受哪些输入参数,每个参数都有其数据类型。而返回类型则表示函数执行完毕后将返回什么类型的值给调用者。在上面的例子中,add
函数接受两个整数作为参数,并返回一个整数值。
内联函数(Inline Function)
内联函数是一种优化手段,通过告诉编译器将函数调用处的代码替换成函数体内的代码,从而避免函数调用时带来的栈帧切换等开销,提高程序运行效率。内联函数使用关键字inline
进行声明,但是否真正实现内联取决于编译器的决定。
inline int max(int x, int y) {
return (x > y) ? x : y;
}
默认参数(Default Arguments)
在定义函数时,可以为部分或全部参数指定默认值,这样在调用函数时可以省略这些参数,提供了更大的灵活性。以下是一个具有默认参数的函数示例:
double calculateArea(double radius = 1.0, double height = 1.0) {
return 0.5 * M_PI * radius * radius * height;
}
函数重载(Function Overloading)
C++允许在同一作用域内为同一个函数名定义多个不同的版本,只要这些版本的参数列表不同即可。这被称为函数重载,是面向对象编程中多态性的体现之一。
// 函数重载示例
void print(int value);
void print(double value);
void print(string text);
// 实现
void print(int value) {
cout << "Integer: " << value << endl;
}
void print(double value) {
cout << "Double: " << value << endl;
}
void print(string text) {
cout << "Text: " << text << endl;
}
类与对象
类与对象在C++编程中扮演着至关重要的角色,它们是实现面向对象设计的核心机制。类(class)是一种用户自定义的数据类型模板,它不仅能够封装数据成员(也称为成员变量或属性),还可以封装用于操作这些数据的成员函数(又名方法)。通过类,程序员可以创建具有特定行为和状态的对象实例,从而实现了数据抽象和信息隐藏。
类的定义
-
类声明:
类是一个蓝图或模板,用于描述具有共同属性(数据成员)和行为(成员函数)的对象集合。例如:
class MyClass { public: int dataMember; void memberFunction(); private: double privateDataMember; };
这里
MyClass
是一个类,它包含公有数据成员dataMember
和成员函数memberFunction()
,以及私有数据成员privateDataMember
。 -
构造函数与析构函数:
构造函数用于当创建对象时自动调用,初始化对象的状态,其名称与类名相同,没有返回类型:
ClassName::ClassName(int initVar) {
privateVariable = initVar;
// 其他初始化操作
}
默认构造函数:如果没有显示声明构造函数,编译器会生成一个默认构造函数,不做任何特殊初始化。
拷贝构造函数:当用一个已存在的对象去初始化新对象时会被调用,形如 ClassName(const ClassName &other)
。
析构函数则在当对象生命周期结束时自动调用,用于释放对象占用的资源。例如:
ClassName::~ClassName() {
// 释放资源等清理工作
}
对象的创建与使用
-
对象实例化: 通过类创建对象的过程称为实例化。实例化时会自动调用构造函数来初始化对象。
-
成员访问: 对象可以直接访问其公有和保护成员,私有成员只能通过类内部的成员函数进行访问。
obj.dataMember = 20; // 访问并修改公有数据成员 obj.memberFunction(); // 调用公有成员函数
访问权限控制
C++提供了三种访问权限修饰符来控制类内部数据和函数的可见性:public
、private
和 protected
。
-
public
:声明为public的成员可以从类外部以及类内部自由访问。
-
private
:声明为private的成员只能从类内部访问,类外部无法直接访问,这是为了保护类内部数据的安全性和完整性,防止意外修改。
-
protected
:声明为protected的成员可以在派生类中访问,但在类外部仍然不可见,这有助于实现继承中的部分封装。
面向对象三大特性
面向对象编程的三大基本特征是封装、继承和多态。
-
封装(Encapsulation):
-
封装是面向对象编程的基本特性之一,它通过将数据(属性)和操作处理这些数据的函数(方法)捆绑在一起,形成一个独立的实体——类。并限制对数据的直接访问,仅提供公共接口以操纵数据,确保了数据安全和内聚性。在C++中,通过访问修饰符(public, private, protected)实现封装。
- 实例说明:
class Car { private: string model; // 私有成员变量,外部无法直接访问 int year; public: // 公共构造函数,用于初始化私有成员变量 Car(string m, int y) : model(m), year(y) {} // 公共成员函数,对外提供操作私有数据的方法 void setModel(string newModel) { model = newModel; } string getModel() const { return model; } };
在这个例子中,
model
和year
是私有成员变量,只能通过类提供的公共接口(如setModel()
和getModel()
)来访问或修改它们的值,从而保证了内部状态的安全性。
-
-
继承(Inheritance):
-
继承是一种类与类之间的关系,允许一个类(子类或派生类)从另一个类(基类或父类)继承已有的状态和行为,并可在此基础上扩展新的功能或重写已有功能。这样可以减少代码重复,提高代码复用率,形成类的层次结构。C++支持单继承和多继承。
- 实例说明:
class Vehicle { // 基类(父类) public: virtual void run() { cout << "Vehicle is running." << endl; } }; class Car : public Vehicle { // 派生类(子类) public: void run() override { cout << "Car is running with its engine." << endl; } };
在这里,
Car
类继承自Vehicle
类,并覆盖了run()
虚函数。这样,当通过 Car 类型的对象调用run()
时,会执行子类重写的方法。
-
-
多态(Polymorphism):
-
多态是指同一消息可以根据发送对象的不同而产生不同的行为。在C++中,有两种形式的多态:静态多态(编译时多态,通过函数重载和运算符重载实现)和动态多态(运行时多态,通过虚函数和纯虚函数实现)。动态多态使得基于基类指针或引用调用派生类对象的方法时,能根据实际对象类型调用相应的函数。
- 实例说明:
class Animal { public: virtual void makeSound() = 0; // 纯虚函数,定义抽象类 }; class Dog : public Animal { public: void makeSound() override { cout << "Woof!" << endl; } }; class Cat : public Animal { public: void makeSound() override { cout << "Meow!" << endl; } }; int main() { Animal* animalPtr; Dog dog; Cat cat; animalPtr = &dog; animalPtr->makeSound(); // 输出"Woof!" animalPtr = &cat; animalPtr->makeSound(); // 输出"Meow!" return 0; }
上述代码展示了动态多态,通过指向基类的指针调用不同子类的对象,根据实际类型决定调用哪个版本的
makeSound()
方法。
-
总结:
通过详尽地剖析C++程序的基本结构和基本语法,我们初步了解了构建C++程序的关键要素。然而,这仅仅只是C++庞大体系的冰山一角,后续的学习还将涵盖模板元编程、命名空间、异常处理、STL标准库等内容。在实践中不断尝试和理解这些概念,逐步提高编程技能,你将会发现C++世界无尽的魅力所在。
请继续关注本专栏的更新内容,下一章节中我们将进一步深入C++的学习之旅,探索更高级的主题和特性。加油,每一位正在踏上C++学习之旅的朋友们!期待你们在C++的世界里收获满满的成就感与知识财富!
感谢你的观看!有什么不足请在评论区发言告诉我,让我们一起进步吧!(鼓励一下让作者加油创作吧!)
Tip:为了获得更深入的学习体验,请参考相关教程或书籍,了解C++语言的更多基本结构和基本语法。
每篇图片分享
图片来自inscode上的开源程序
濒危动物:金狮狨