C++编程基础

C++的概述

C++和C的区别

C++是面向对象的语言,C是面向过程。
面向过程编程(Procedural Programming)是一种程序设计范式,其核心思想是将程序分解为一系列独立的函数,每个函数代表一个具体的操作,程序的执行过程是按照函数调用的顺序依次执行的。
相比之下,面向对象编程(Object-Oriented Programming,OOP)则是一种更加高级的程序设计范式,其核心思想是将程序看作是一组相互作用的对象,每个对象具有自己的数据和操作,对象之间通过消息传递进行交互。

C++基础的程序结构

基础的程序


程序的运行结果
Hello World!

程序的结构说明

#include<iostream>语句说明
其中#include指令是文件包含指令,作用是去引入头文件进来。上述语句的作用就是引入iostream头文件进来。
为了方便程序员编程,C++提供了许多已经写好的函数、类等这些程序,程序员可以直接使用,而这些写好的类、函数会被写在头文件里,程序需要引入对应的头文件,才能使用头文件里已经写好的程序。
iostream是一个最基本的头文件,基本上C++程序都需要引入这个头文件。

using namespace std;语句说明
主要作用就是释放std这个命名空间。iostream头文件里的许多声明定义都是在std命名空间里写的,释放std之后,我们就可以直接用iostream里声明定义的东西。(具体的解释说明,后面说讲到命名空间里会说。)

int main() 语句说明
int main是main函数,是C++程序的入口,每个C++程序必须要有一个main函数。程序在执行时候,会去执行main函数里写的语句程序。

C++的编译执行

C++程序的执行原理

高级语言程序(称为源程序)虽然编写方便,但计算机不能直接执行,必须经过一定的软件(例如解释程序、编译和连接程序等)对其进行加工,生成由机器指令表示的程序(称为目标程序),然后才能由计算机来执行。这种加工过程可以分为编译和解释两种方式。

C++编译执行的过程

整个编译的过程可以大致的分为编译和连接两个步骤,编译阶段将源程序转换成目标文件,连接阶段将目标文件连接成可执行文件。

也可将整个编译执行过程可以细致的划分为,预处理(预编译阶段)、编译、汇编、链接四个阶段。

预处理阶段

该阶段主要处理源程序中的预处理指令。
具体就是处理宏定义指令 #define 、头文件包含指令 #include 、条件编译指令 #ifdef 等 ,在这个阶段,预处理器会读入源程序并执行预处理指令,然后将预处理后的源程序生成一个新的中间文件。

编译阶段

该阶段主要是进行语法检查,将预处理阶段生成的中间文件,转换为汇编语言。
编译器会将源程序中的代码转换成汇编语言,并生成一个目标文件。

汇编阶段

该阶段将编译阶段生成的目标文件,从汇编语言转换为机器语言。
汇编器会将编译器生成的目标文件里面的汇编语言转换为机器语言,并生成一个可重定位目标文件(obj文件)。

链接阶段

该阶段主要将可重定位目标文件和库文件链接成一个可执行文件。
链接器会将编译器和汇编器生成的目标文件(obj文件),和库文件(调用的库函数代码或自定义文件代码)链接起来,生成一个可执行文(exe文件)。

静态链接和动态链接的区别

静态链接(static linking)是指在编译链接阶段,将所需要的库文件的代码全部复制到目标文件中,生成一个独立的可执行文件,这个可执行文件就包含了所有需要的函数和库文件代码。
目标文件中使用的所有函数和库函数的代码都被复制并包含在最终的可执行文件中,所以在运行这个可执行文件时不需要再加载任何库文件。
这样做的好处是,可以直接在目标机器上运行,不需要依赖外部的库文件。
缺点是这种方式会造成可执行文件的体积较大。

动态链接(dynamic linking)是指在编译链接阶段只把库文件相关信息记录在可执行文件中,而不是把库文件的代码全部复制到可执行文件中。
在运行时,当需要调用库函数时,操作系统会自动从磁盘上加载相应的库文件,将库文件中的代码加载到内存中,然后进行链接。
因此,这种方式可以减小可执行文件的体积,减少磁盘空间的占用。
缺点是需要保证在运行时可执行文件所依赖的库文件必须存在,否则程序就无法运行。

总的来说,静态链接生成的可执行文件比较大,但是独立性强,不需要依赖库文件;动态链接生成的可执行文件比较小,但是依赖于系统的共享库文件,如果这些库文件不存在或者版本不兼容,就会导致程序无法运行。

源程序(源文件)的理解

C++中源程序指的是由C++语言编写的文本文件,其中包含程序的源代码,即一系列的指令和语句。通常以.cpp等扩展名结尾。

目标文件

目标文件是编译器将源文件编译成机器代码后生成的文件,这些文件以二进制形式存储着机器码、符号表以及其他与目标文件有关的信息。

可执行文件的理解

最终生成的可执行文件就是可以运行的程序,它包含了所有的代码和数据。当我们运行程序时,操作系统会将可执行文件加载到内存中,并执行其中的代码。

注意

  1. 现阶段,许多C++编辑器在写好C++程序后,点击编译按钮,则会完成整个编译过程生成一个可执行文件
  2. 编译器会将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个可执行文件。
  3. 编译器会将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个可执行文件。
  4. 头文件本身不参与编译过程。

C++输入与输出

cin的理解

cin表示输入流对象,它是输入/输出流库的一部分。与cin相关联的设备是键盘,用法如下
cin>>V1>>V2>>... >>Vn;
其中,>>称做提取运算符, V1、V2,、.. . ,Vn都是事先定义好的变量。此语句的执行功能是,程序暂时中止执行,等待用户从键盘上输入数据。当用户输入了所有的数据后,应按回车键结束输人,程序会将用户输入的数据形成输入数据流,用提取运算符>>将该数据流存储到各个变量中,并继续执行后续的语句。

举例

cin>>p>>q;
就是要求用户分别为变量p和q各输人一个数据。在输入时,应注意用空格、Tab键或回车键将所输入的数据分隔开(注意不能使用逗号、分号或其他符号间隔)。

cout的理解

cout表示输出流对象,它也是输人/输出流库的一部分,与cout相关联的设备是显示器,其基本用法如下:
>cout<<E1<<E2<<... <<Em;
其中<<称作插人运算符,E1、E2、… Em可以是常量、变量或表达式。
此语句的执行功能是,将各个常量、变量或表达式的值输出(显示)到显示设备上。

标识符

标识符的理解

标识符是程序中变量、类型、函数和标号的名称,可以由程序设计者命名,也可以由系统指定。标识符由字母,数字和下画线”_“组成。

标识符规则

  1. 第一个字符不能是数字。与FORTRAN和 BASIC等程序设计语言不同
  2. C++的编译器把大写和小写字母当作不同的字符,即区分字母的大小写。
  3. 不同的C++编译器对在标识符中最多可以使用多少个字符的规定各不相同,ANSI标准规定编译器应识别标识符的前6个字符。

关键字

在C++中,有些标识符具有专门的意义和用途,它们不能当做一般的标识符使用,这些标识符称为关键字。
主要有:

编译预处理指令单词

C++还使用了下列12个标识符作为编译预处理的命令单词
define、elif、else、endif、error、if、ifdef、ifndef、include、line、progma、undef
并赋予了特定的含义。程序员在命名变量﹑数组和函数时也不要使用它们。

标点和特殊字符

在C++字符集中,标点和特殊字符有各种用途,包括组织程序文本、定义编译器等。有些标点符号也是运算符,编译器可从上下文确定它们的用途。C++的标点和特殊字符如下:

这些字符在C++中均具有特定的含义,程序员在命名时也是不能用他们的。

编程思路

编程的步骤


左值引用和右值引用

左值理解

左值(lvalue)是指具有标识符的表达式,即可以在赋值操作符的左边出现的表达式。左值可以是变量、对象、函数等,它们具有持久性并且可以被取地址。

右值理解

右值(rvalue)是指临时的、无法被修改的表达式,它们不能出现在赋值操作符的左边。右值可以是字面常量、表达式的结果、临时对象等。

左值和右值的区别

简单来说,左值是可以被取地址并且具有持久性的表达式,而右值是临时的、无法被修改的表达式。

左值引用的定义形式

左值引用使用 & 符号来声明,例如 int& ref = variable;
左值引用只能绑定到左值,即具有持久性的、具名的对象。

右值引用的定义形式

右值引用使用 && 符号来声明,例如 int&& rref = 42;
右值引用可以绑定到右值,即临时的、无法被修改的值,如字面常量、表达式结果和临时对象。

右值引用举例

1
2
3
4
int&& rvalueRef = 42;  // 绑定到右值
int x = std::move(rvalueRef); // 移动 rvalueRef 中的值到 x

在上述示例中,rvalueRef 是一个右值引用,可以绑定到右值 42,然后使用 std::move 将其值移动到变量 x 中。

右值引用的目的

右值引用的引入主要是为了支持移动语义,即将资源从一个对象转移给另一个对象,而不是进行资源的拷贝操作,从而提高程序的性能和效率。
右值引用也支持完美转发

extern “C”

extern “C”的理解

extern “C” 的作用,就是为了能够实现在C++代码里正确的调用C语言代码。也就是说,在C++代码里,编译器会让在extern “C” 里的代码按C语言的方式进行编译,而不是按照C++的方式进行编译。

而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

extern “C”使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++ 代码
#include <iostream>

extern "C" {
void myCFunction(int num) {
std::cout << "Calling C function with num = " << num << std::endl;
}
}

int main() {
myCFunction(42); // 调用 C 函数
return 0;
}
在上面的示例中,我们在 C++ 代码中定义了一个 extern "C" 块,在其中声明了一个名为 myCFunction 的函数。该函数是由 C 代码提供的,并在 C++ 代码中进行调用。使用 extern "C" 使得 C++ 编译器不会对函数名进行修饰,从而使其与 C 代码兼容。

C和C++ 编译器编译时对函数名的处理

C++ 编译器在编译过程中,根据函数的参数类型、返回类型等信息,对函数名进行一定的改写或加上特定的标识,以区分不同的函数。
这个修饰过程称为名称修饰(Name Mangling)。这是为了支持C++的函数重载和命名空间等特性。
由于 C++ 支持函数重载,即可以有多个同名函数但参数类型或个数不同,为了能够区分这些函数,编译器需要在编译时对函数名进行改写,加上一些标识信息,使得这些函数的名称在目标文件中是唯一的,因此编译器编译函数的过程中会将函数名进行一定的改写。

C语言并不支持函数重载,因此编译C语言代码的函数时,不会对函数名进行修饰。

C++ 编译器对函数名进行修饰的举例

1
2
3
4
5
6
7
8
9
例如,对于下面的 C++ 函数声明:
void myFunction(int a);
void myFunction(float a);

C++ 编译器会根据参数类型的不同,对函数名进行修饰,使其在目标文件中具有不同的名称,例如:
_myFunction_i // 对应 void myFunction(int a)
_myFunction_f // 对应 void myFunction(float a)


使用 extern "C" 是为了确保 C++ 编译器将函数声明和定义按照 C 的方式进行处理,以兼容 C 语言的链接规则。它的作用主要有两个方面:

  1. 去除 C++ 的名称修饰:C++ 编译器对函数名进行修饰,加上一些额外的标识,用于区分函数重载和命名空间等特性。但在与 C 代码进行链接时,C 编译器不会对函数名进行修饰,因此需要使用 extern "C" 告诉 C++ 编译器不要对函数名进行修饰,保持与 C 代码的函数名一致。
  2. 保持链接规则一致:C 和 C++ 有不同的函数链接规则。C 函数使用的是 C 链接规则,即使用 C 语言的名字解析和链接方式;而 C++ 函数使用的是 C++ 链接规则,即使用 C++ 的名字解析和链接方式。通过使用 extern "C",可以告诉 C++ 编译器使用 C 链接规则来处理函数链接,保持与 C 代码的链接规则一致。

内存泄漏

内存泄漏的理解

内存泄漏指的是在程序运行过程中,分配的内存空间没有被正确释放或释放的时机不当,导致这部分内存无法再被程序访问和利用,从而造成内存资源的浪费。

数据类型

数据类型的基础知识

数据类型的理解

计算机在处理数据时,所有的数据最终都以二进制形式在内存中存储。数据的类型,决定了数据在内存中的存储方式、存储长度以及其输出形式。
例如,数据存储内存时,整数类型int通常使用4个字节(32位)来存储,而浮点数类型float可能使用4个字节(32位),double则通常使用8个字节(64位)。
数据输出时,整数类型可能以十进制形式输出,而浮点数类型可能以浮点数形式输出。

基本数据类型的类型说明符

类型说明符指出了变量、函数返回值的数据类型,该类型决定了变量、函数返回值的格式。下面是一些基本数据类型的类型说明符:
char:字符类型
short:短整型
int:整型
long:长整型
float:浮点类型
double:双精度类型
bool:布尔类型

不同数据类型的存储形式

字符数据和字符串数据


字符常数表现形式:既可以用字符表示也可以用其ascii码(整数)表示



不同类型数据参与运算

不同类型数据参与运算的规则

C++规定,不同类型的数据在参加运算之前会自动转换成相同的类型,然后再进行运算。运算结果的类型也就是转换后的类型。转换的规则如下:
级别低的类型转换为级别高的类型。

各类型按级别由低到高的顺序为char,int,unsigned(无符号整型),long , unsigned long(无符号长整型),float , double。

例如,将一个char类型的数据和一个int类型的数据进行运算,其结果为int型;
一个int 型的数据和一个double型数据的运算,结果的类型为double型。

另外,C++规定,有符号类型数据和无符号类型的数据进行混合运算,结果为无符号类型。例如, int型数据和unsigned类型数据的运算结果为unsigned型。

对于赋值运算来说,如果赋值运算符右边的表达式的类型与赋值运算符左边的变量的类型不一致,则赋值时会首先将赋值运算符右边的表达式按赋值运算符左边的变量的类型进行转换,然后将转换后的表达式的值赋给赋值运算符左边的变量。整个赋值表达式的值及其类型也是这个经过转换后的值及其类型。

举例说明

int a;
a=1+0.5;
这时候1是整型数据,0.5是float型数据,两者之间,最高的数据类型是float,所以相加前,两者都会转为float数据类型,而且运算结果也是float数据类型。

然后就是要把1.5赋值给a,而a是int数据类型,1.5是float数据类型,就会把1.5转为int数据类型(转换的过程会自动切除掉小数点后面的数字),最终a会等于1。

类型修饰符signed和unsigned

signed和unsigned的理解

基本类型说明语句的前面还可以加上各种修饰符。修饰符用来改变基本类型的意义,以便更准确地适应各种情况的需求。C++提供的修饰符如下:
signed//有符号
unsigned//无符号

signed的意义为带符号。由于基本类型char,short ,int ,long 等均为带符号位的类型,因此signed修饰符的用途不大。
unsigned适用于char , short , int和 long四种整数类型,其意义为取消符号位,只表示正值。

signed和unsigned修饰int

当类型修饰符应用于int类型之前时,可以省略int不写(即int是隐含表示的)。例如:
signed int等价于signed
unsigned int 等价于unsigned

实际上,前面所讲的long和 short 也是类型修饰符,只不过是省略了后面的int。
如果将long用于double之前,会形成―种新的数据类型long double,在有些系统中它可以提供比double类型更多的存储空间。

C风格强制类型转换操作符

强制类型转换操作符的理解

在程序中使用强制类型转换操作符可以明确地控制数值的数据类型转换,使数值的数据类型,转换成其他的数据类型。

强制类型转换操作符的形式

两种形式形式,一种是C++保留的C语言形式,由一个放在括号中的类型名组成,置于表达式或变量之前,即
(类型名)表达式
其结果是表达式的类型被转换为由强制类型转换操作符所标明的类型。
例如,如果i的类型为int ,表达式( double)i将i强制转换为double类型。

另一种方式是C++新增加的形式,类型名不加括号,而将表达式或变量以括号括起来,即
类型名(表达式)
例如, double( i)也可以将i强制转换为double类型。

C++的四种强制类型转换

静态转换

静态转换(Static Cast)

静态转换可以用于基本类型之间的转换,它可以将一个表达式的类型转换为另一种相关类型,例如将整数转换为浮点数。也可以用于具有继承关系的类之间的转换,它可以在派生类和基类之间进行转换。
它还可以用于其他类型的转换,例如将一个枚举类型转换为整数类型,或者将一个指针转换为void指针等。

它在编译时转换,即不提供运行时的安全检查。如果进行的转换是不安全的,例如将一个指针转换为错误类型的指针,静态转换无法提供保证,可能导致程序出现未定义行为。

静态转换基本类型转换举例

1
2
3
int a = 10;
double b = static_cast<double>(a); // 将整数转换为浮点数

静态转换具有继承关系的类之间的转换举例

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
class Base {
public:
void doSomething() {
// 基类的成员函数
}
};

class Derived : public Base {
public:
void doSomethingExtra() {
// 派生类的额外成员函数
}
};

int main() {
Derived derivedObj;

// 将派生类对象转换为基类指针
Base* basePtr = static_cast<Base*>(&derivedObj);

// 可以通过基类指针调用基类的成员函数
basePtr->doSomething();

return 0;
}
需要注意的是,静态转换在进行类之间的转换时,没有运行时的类型检查。这意味着如果将基类指针或引用转换为错误的派生类指针或引用,静态转换也会成功,但可能导致程序出现未定义行为。因此,在进行类之间的转换时,需要确保转换是安全和合理的。

动态转换

动态转换(Dynamic Cast)的理解

动态转换主要用于继承关系的类之间进行转换,且这些类的基类得具有是多态性。基类具有多态性即包含虚函数,那么就可以将基类指针或引用,动态转换为派生类指针或引用。
它在运行时转换,即在运行时进行类型检查,如果转换是合法的,则执行转换,否则返回空指针(对于指针转换)或引发std::bad_cast异常(对于引用转换)。

动态转换举例

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
class Base {
public:
virtual void doSomething() {
// 基类的成员函数
}
};

class Derived : public Base {
public:
void doSomethingExtra() {
// 派生类的额外成员函数
}
};

int main() {
Base* basePtr = new Derived();

// 使用动态转换将基类指针转换为派生类指针
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

// 检查转换结果是否为空
if (derivedPtr != nullptr) {
// 可以通过派生类指针调用派生类特有的成员函数
derivedPtr->doSomethingExtra();
} else {
// 转换失败,对象实际上不是派生类的实例
// 进行相应的处理
}

delete basePtr;

return 0;
}
此示例中,`basePtr` 是基类指针,指向派生类对象。通过使用 `dynamic_cast` 进行动态转换,将 `basePtr` 转换为派生类指针 `derivedPtr`。由于对象实际上是派生类的实例,因此转换成功,`derivedPtr` 不为空。可以通过 `derivedPtr` 调用派生类特有的成员函数 `doSomethingExtra()`。

对于没有多态性的基类指针,是无法使用动态转换的。



注意区分两种情况
第一种:
Base* basePtr = new Derived();
表示创建了一个派生类对象,并用基类指针 basePtr 指向该对象。由于派生类是基类的一种特殊情况,因此这是有效的。

然后使用 dynamic_cast 进行动态转换
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
这是将基类指针 basePtr 转换为派生类指针 Derived*。由于派生类指针可以指向基类对象,因此这个转换是有效的。
在这个情况下如果转换成功,即 `derivedPtr` 不为空指针,则可以通过 `derivedPtr` 调用派生类 `Derived` 中的所有成员函数,包括基类继承的成员函数和派生类自己定义的成员函数


第二种:
Base* basePtr = new Base();
表示创建了一个基类对象,并用基类指针 basePtr 指向该对象。

然后使用 dynamic_cast 进行动态转换
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
这是将基类指针 basePtr 转换为派生类指针 Derived*。由于基类对象不是派生类的实例,这个转换将失败,返回一个空指针

不具有多态的转换举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
情况1:
如果 Base 不是多态类,而 Derived 是其派生类
Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr);

尽管 basePtr 实际指向的是 Derived 类的对象,但是由于 Base 类不是多态类,没有虚函数的情况下,使用 static_cast 进行类型转换并不会检查实际运行时对象的类型。

在这种情况下,编译器只会简单地将基类指针 basePtr 转换为派生类指针 Derived*,而不会检查转换的有效性。这意味着无论转换是否有效,都不会引发编译器错误或警告。

因此,当 Base 不是多态类时,使用 static_cast 进行类型转换是合法的,但是需要注意确保转换的正确性,否则可能导致程序出现未定义的行为或错误的结果。


情况2:
如果 Base 不是多态类,而 Derived 是其派生类
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

由于 Base 不是多态类,没有虚函数,因此 dynamic_cast 无法在运行时执行类型检查,转换操作将会失败。

在这种情况下,dynamic_cast 返回的指针将会是一个空指针,即 derivedPtr 将会是 nullptr。这是因为 dynamic_cast 在进行类型转换时会进行运行时的类型检查,如果发现转换是不安全的或不合法的,则返回空指针。


常量转换

常量转换(Const Cast)的理解

const_cast用于去除常量属性,允许对常量对象进行修改。
它主要用于将const限定符添加或移除,以便在某些情况下更方便地操作对象。
他是编译时转换,即不会在运行时检查类型

引用的常量转换的举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void modifyValue(const int& value) {
int& mutableRef = const_cast<int&>(value);
mutableRef = 10; // 修改原本被声明为常量的值
}

int main() {
int a = 5;
const int& ref = a;
modifyValue(ref);
// 现在 a 的值变为 10
return 0;
}
在上述代码中,modifyValue 函数接受一个常量引用参数 value,然后使用 const_cast 将其转换为可修改的非常量引用 mutableRef。通过 mutableRef 修改了原本被声明为常量的值。

指针的常量转换的举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void printValue(int* value) {
std::cout << "Non-const version: " << *value << std::endl;
}

void printValue(const int* value) {
std::cout << "Const version: " << *value << std::endl;
}

int main() {
int a = 5;
const int* constPtr = &a;
printValue(const_cast<int*>(constPtr)); // 调用非常量版本的函数
return 0;
}
在上述代码中,存在两个重载的函数 printValue,一个接受 int* 参数,另一个接受 const int* 参数。通过使用 const_cast 将 constPtr 转换为 int*,可以调用非常量版本的函数。
需要注意的是,const_cast 应该谨慎使用,因为它可能导致未定义行为或破坏程序的常量性质。应该只在确保安全的情况下使用 const_cast 进行常量转换。

未定义行为的理解

未定义行为(Undefined Behavior)是指在程序运行过程中,根据C++标准,无法确定程序的行为的情况。

常见未定义行文

在C++中,一些常见的引起未定义行为的情况包括:

  1. 未初始化变量的使用:使用未初始化的变量的值会产生未定义行为。变量应该在使用之前进行初始化。
  2. 数组越界访问:访问数组元素时超出了数组的范围也会导致未定义行为。应该确保数组索引在有效范围内。
  3. 空指针解引用:解引用空指针会导致未定义行为。在使用指针之前应该进行有效性检查。
  4. 对已释放的内存进行访问:释放了内存后仍然使用指向该内存的指针也会产生未定义行为。应该注意正确管理内存的生命周期。
  5. 整数溢出:当整数运算导致结果超出类型的表示范围时,会产生未定义行为。应该谨慎进行整数运算。
  6. 多个修改同一变量而没有同步:当多个线程或并发操作同时修改同一变量而没有适当的同步机制时,会产生未定义行为。

重新解释转换

重新解释转换(Reinterpret Cast)的理解

reinterpret_cast用于不相关类型之间的转换,例如将指针转换为整数类型或将整数类型转换为指针类型。它提供了一种低级别的转换,没有类型检查,很容易导致错误。
它在编译时转换,即不会在运行时检查类型。

reinterpret_cast将指针转换为整数使用举例

1
2
3
4
int* ptr = new int(42);
uintptr_t value = reinterpret_cast<uintptr_t>(ptr); // 将指针转换为无符号整数
在上述示例中,通过 reinterpret_cast 将 int 类型的指针 ptr 转换为 uintptr_t 类型的整数 value。

将整数转换为指针的使用举例

1
2
3
uintptr_t value = 12345;
int* ptr = reinterpret_cast<int*>(value); // 将整数转换为指针
在上述示例中,通过 reinterpret_cast 将 uintptr_t 类型的整数 value 转换为 int 类型的指针 ptr。

类型间的指针转换使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
void foo() {
// 类 A 的成员函数
}
};

class B {
public:
void bar() {
// 类 B 的成员函数
}
};

A* aPtr = new A();
B* bPtr = reinterpret_cast<B*>(aPtr); // 将指向 A 类的指针转换为指向 B 类的指针
在上述示例中,通过 reinterpret_cast 将指向类 A 的指针 aPtr 转换为指向类 B 的指针 bPtr。需要注意的是,这种类型转换在语义上是不安全的,因为类 A 和类 B 之间可能具有不同的内存布局和成员函数,因此使用 reinterpret_cast 进行类指针的转换需要谨慎,确保转换的正确性。

总之,reinterpret_cast 允许进行底层的重新解释转换,但需要注意潜在的安全问题和平台依赖性。

地址间的强制转换类型

结构体的各个数据地址的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
Struct T
{
A a;
B b;
C c;
}
里面有三个数据类型ABC,每次创建一个结构体的对象,他就会再内存里连续的开辟一个空间,而这个对象的空间的大小=数据类型A的大小+B的大小+C的大小,那么在开辟的这个空间里,他会依次把空间大小分配a,b,c。
假设创建一个结构体对象t,
a的空间地址为d1d2,地址长度为2。
b的空间地址为d3d4d5,地址长度为3。
c的空间地址为d6d7d8d9,地址长度为4。
t的地址就等于d1d2d3d4d5d6d7d8d9,地址长度为9。也就是&t等于d1d2d3d4d5d6d7d8d9。

查看数据的数据类型方式(地址的数据类型)

T *s=&t,也就是说t地址能赋值给s,说明t的地址(即&t)的类型为T*类型。通过这个例子,我们可以知道,某种类型数据的地址的数据类型,就是这个类型数据+*。比如int h,h为int数据类型,&h的数据类型就是int*

不同类型的地址之间转换的效果

不同数据类型地址间的强制转换,就是在首地址不变的情况下,地址长度的转换。
T*类型的地址长度为9,A*类型的地址长度为2。T*转换成A*地址类型,则首地址不变,地址长度变为2。同理A*转成T*,其地址长度从2变为9,首地址不变。

不同类型的地址之间转换举例

地址数据转换类型(A*)&t,这个意思就是说把&tT*数据类型,转换成A*数据类型。 &t原本的数据类型是T*,首地址为的d1,其地址长度为9,那么&t就等于d1d2d3d4d5d6d7d8d9。 转成A*数据类型后,首地址不变,长度变为2,则(A*)&t就为d1d2.

相反(T*)&a,把&aA*数据类型转成T*数据类型。&a原本数据类型为A*,首地址d1,地址长度为2,则&A为d1d2。转成T*类,首地址不变,地址长度变为9,(T*)&a就为d1d2d3d4d5d6d7d8d9

C++的四种

常量

常量的基础知识

常量的理解

C++的数据有两种基本形式,一是常量,一是变量。 常量是在程序运行的整个过程中其值始终不可改变的量。
常量可以是整型、实型、字符、字符串和布尔类型。

比如
在C++中,我们写的具体的数值,整数常量10、实数常量3.14和字符常量’A’,字符串常量”Hello”。

常量在程序运行期间不可更改。

常量的类型

常量有两种基本类型:字面常量和符号常量。字面常量是程序中直接出现的常量值,例如整数常量10、实数常量3.14和字符常量’A’。符号常量则是在程序中定义的常量,通过使用#define预处理器指令或const关键字来定义,它们在程序中被用于代替具体的数值,从而提高程序的可读性和可维护性。

常量的后缀

常量的数据类型

常量通过本身的书写格式就说明了该常量的类型。

无后缀数值

当什么后缀都没写时,则根据有无小数点及位数来决定其具体类型,如:123表示的是有符号整型数,而12341434则是有符号长整型数;而34.43表示双精度浮点数。

有后缀数值

在C++中,数字表达提供了一系列的后缀进行表示,如下:
u或U  表示数字是无符号整型数,如:123u,但并不说明是长整型还是短整型

l或L  表示数字是长整型数,如:123l;而123ul就是无符号长整型数;而34.4l就是长双精度浮点数,等效于双精度浮点数

i64或I64  表示数字是长长整型数,其是为64位操作系统定义的,长度比长整型数长。如:43i64

f或F  表示数字是单精度浮点数,如:12.3f 

e或E  表示数字的次幂,如:34.4e-2就是0.344;0.2544e3f表示一个单精度浮点数,值为254.4    

常量修饰符const

const的理解

如果要表示某个变量的值不能修改,可使用常量修饰符const,通过const修饰的变量称为常量变量。
使用const修饰的变量在定义时必须进行初始化赋值,否则会导致编译错误。因为const变量的值在定义之后就不能再被修改,所以必须在定义时就赋一个初始值。

语法格式

const <类型说明符><常量名> = <常量值>;

举例

注意

使用const修饰的变量实际上是常量,不能被程序改变,因此在声明时一定要进行初始化赋值。常量变量一经生成,其值再不能改变,如果在以后的执行语句对常量变量进行赋值就会导致编译错误。
例如:

除此之外,常量修饰符还可用于修饰函数的参数。
例如:

表示在函数体内参数arr和 count的值不能改变,不允许出现对它们的赋值操作。

常量表达式的修饰符constexpr

constexpr的理解

constexpr 是 C++11 引入的关键字,用于声明一个函数或变量为常量表达式。常量表达式是在编译时可以被计算出来的表达式,因此具有编译时计算的特性。被声明为 constexpr 的函数和变量可以用于需要编译时常量的场合,例如数组大小、模板参数等。
使用constexpr关键字修饰函数,可以指示编译器在编译期间就对函数进行求值,从而可以在编译期就得到结果,避免了运行时的开销。

函数用constexpr声明的条件

对于函数而言,只有满足以下条件的函数才能被声明为 constexpr

  • 函数的返回值和参数的类型必须都是字面值类型(literal type)。
  • 函数体必须只包含简单语句,不能有循环、分支等复杂的控制语句。
  • 函数体内只能调用其他的 constexpr 函数。

constexpr使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
//例如,下面的代码定义了一个 `constexpr` 函数 `square`,用于计算一个整数的平方:
constexpr int square(int x) {
return x * x;
}

//在编译时,如果使用了 `square` 函数,那么传入的参数必须是一个常量表达式,例如:
constexpr int y = square(5); // 正确,编译时计算出 y 的值为 25
int z = square(5); // 错误,z 不是常量表达式


//注意,即使声明了 `constexpr`,如果在使用过程中传入的不是常量表达式,仍然会在运行时进行计算,而不是在编译时进行计算。


八进制常量

八进制常量书写格式

八进制常量书写格式以0打头,后面是八进制数。例如:
072、066,0377,06660

八进制的作用

八进制数主要用于表示转义字符。用八进制常数构造转义字符时要使用3位八进制数,最前面用于表示八进制数的0可以省略,但不足3位时前面要补0。
用八进制数构造的转义字符不仅可以表示控制字符、ASCII 码,还可以表示代码值在128 ~255之间的扩展ASCII码。例如,如果用八进制常数表示一个值为157的扩展ASCII码(人民币元符号¥) ,因为十进制的157换算为八进制数就等于0235 ,所以其转义字符为’\235’。

二进制转为八进制的方法

如果要将二进制数换算为八进制数,首先将其由右向左每3位分成1组,然后分别将各组换算为1位八进制数,最后在结果的最前面加上一个表示八进制数的0即可。

例如,要将二进制数10011101换算为八进制数,首先将其分为3组:
10,011,101,然后将各组直接用一位八进制数表示。
二进制的10,011和101换算为八进制数后分别等于2、3和5.因此二进制数10011101换算为八进制数为0235.

八进制转换二进制的方法

将八进制数转换为二进制数也同样简单,只要将八进制数中的各位数转换为3位二进制数即可。在转换时要注意,每个八进制数位一定要转换为3位二进制数,不足3位时要在前面补0。

十六进制常量

十六进制书写格式

十六进制常量书写格式以0x或0X开头,后面是十六进制数。例如:0X12A、0XFFFF、0x7106、0x89AB

二进制转为十六进制的方法

例如,二进制数11111111转换为十六进制数时,首先将二进制数由右向左每4位分为1组:1111,1111
然后将各组分别转换为十六进制数(均为F),并在结果FF的最前面加上表示十六进制数的“0X”,即可得出转换后的结果0XFF。

布尔常量

变量

变量的基础知识

变量的理解

C++的数据有两种基本形式,一是常量,一是变量。
在C++中,变量是程序中用于存储和操作数据的重要元素,它可以存储各种数据类型的值,然后我们可以使用变量的标识符来修改和访问变量存储的值。

它提供了一种将数据与一个特定的标识符相关联的方法,使得我们可以在程序中使用这些数据。

在C++中,我们可以通过赋值语句或其他方法来改变变量的值。变量的值也可以被读取。

注意
变量有自己的数据类型,它定义了变量可以存储的数据的类型和范围。
在使用变量前,得先说明其数据类型,否则程序无法为该变量分配对应的存储空间,即变量要遵循“先说明,后使用”的原则。 这条原则不仅适用于变量,同样适用于C++程序的其他成分,例如函数、类型和宏等。

变量的使用举例

int a =5;
//定义了一个整形变量a,存储了值5。

变量的声明、定义和初始化

变量的定义的理解

为变量分配存储空间就为变量的定义。
注意,在为变量初始化,自然也就完成了变量的定义,因为为变量初始化,自然就给变量分配了存储空间。程序中,变量有且仅有一个定义。

变量的定义举例

int a;
//定义里一个整形变量a

变量初始化的理解

一般而言,使用变量前要对变量进行初始化操作。对变量初始化,就对变量赋值。
这可以通过在变量定义之后使用专门的赋值语句来完成
另一方面,C++也允许在定义变量的同时对变量赋一个初值。例如:
int a=5;
//定义了一个整形变量a,并赋值5。

另外,还有一种初始化变量的方法,其格式如下:
<类型><变量>(<表达式>)
例如int a(5);

注意
如果仅仅定义了某个变量而没有给它赋一定的初值,则该变量的值是一个不确定的量(其具体数值可能和用户所使用的操作系统、运行时间状态等因素有关)。程序员一定要注意这一点,因为一旦疏忽,有时会造成程序逻辑错误。因此,在变量定义时就给它赋初值,是一个比较好的编程习惯。

变量的声明的理解

变量的声明,就是用于向程序表明变量的类型和名字。

变量的声明和定义的关系

1.在C++中定义了变量,也就声明了变量。当定义变量时我们声明了它的类型和名字。例如int i;

2.在C++中声明了变量,不一定定义了变量。比如extern关键字声明变量名而不定义它,即不分配存储空间,此时变量就是只有声明而没有定义,举例, extern int i; 只是声明了却没有定义

3.在声明中并且进行初始化,就被当作定义,即使前面加了extern。例如:extern double pi=3.1416; (只有当extern声明位于函数外部时,才可以被初始化。 )

(总的来说就是 变量的定义=声明+变量分配存储空间))

变量的作用域

作用域的基础知识

变量作用域的理解

每个变量都有一定的有效作用范围,称为作用域,变量只能在其作用域中是可见的,或者说在该区域内是可以使用的,而在作用域以外是不能访问的。

变量根据作用域的划分

根据作用域的不同,可以将C++程序中的变量分为局部变量和全局变量。

局部变量

局部变量的理解

局部变量是在函数或分程序中声明的变量,只能在本函数或分程序的范围内使用。局部变量被分配在堆栈里。
局部变量一般都是自动变量,生存期为程序执行到变量定义域中的期间,局部变量分配存储空间,并设置初值,当程序离开变量定义域,会立即将自动变量占用的存储空间释放

全局变量

全局变量的理解

定义于所有函数之外的变量称为全局变量,可以由本源程序文件中位于该全局变量声明之后的所有函数共同使用。全局变量被分配在全局数据段
全局变量一般是静态变量,在程序运行之前就为变量分配存储空间并设置初值,,全局变量具有和程序执行期相同的生存期

全局变量的用途是在各个函数之间建立某种数据传输通道。通常,在编程时人们大多使用返回值和参数表在函数之间传递数据,这样做的好处是数据流向清晰自然,易于控制,数据也比较安全。
但有时会遇到这种情况,某个数据被许多函数所共用,为了简化函数的参数表,可以将其说明为全局变量。

初看起来全局变量可以被所有的函数所共用,使用简单方便,因此被一些初学者所喜爱,并在程序中大量使用。实际上,滥用全局变量会破坏程序的模块化结构,使程序难于理解和调试,因此要尽量少用或不用全局恋量。

全局变量的扩展和限定

一般来说,全局变量的作用域还可以扩充到其它源程序中,只要在相应的源程序文件中加入外部函数说明语句extern即可。
当然,也可以通过在说明语句之前加上static明确说明某全局变量的作用域仅限于说明该变量的源程序文件中。

代码举例

代码

输出结果
The result in func1 : 100
The result in func2 : 49
x = 0

变量的存储类别

变量存储类别的基础知识

变量根据存储类别的划分

在C++中,根据变量存在时间的不同,可以将存储类别分为4种,即自动(Auto)、静态( Static)、寄存器( Register)和外部(Extern)。

自动变量(auto修饰的变量)

自动变量的理解

自动变量的特点是在程序运行到自动变量的作用域(即声明了自动变量的函数或分程序)中时,才为自动变量分配相应的存储空间,此后才能向变量中存储数据或读取变量中的数据。
一旦退出声明了自动变量的函数或分程序之后,程序会立即将自动变量占用的存储空间释放,被释放的空间还可以重新分配给其它函数中声明的自动变量使用。

因此,自动变量的生存期是从程序进入声明了该自动变量的函数或分程序开始,到程序退出该函数或分程序时结束。在此期间之外自动变量是不存在的。自动变量的初值在每次为自动变量分配存储空间后都要重新设置。

自动变量对存储空间的利用是动态的,通过分配和回收,不同函数中定义的自动变量可以在不同的时间中共享同一块存储空间,从而提高了存储器的利用率。
显然,前面介绍的局部变量(也包括函数的参数)都是自动变量。同样,在整个程序运行过程中,一个自动变量可能经历若干个生存期。而在自动变量的各个不同生存期中程序为该变量分配的存储空间的具体地址可能并不相同,因此在编写程序时,不能期望在两次调用同一个函数时,其中定义的同一个局部变量的值之间会有什么联系

自动变量的声明

一般在函数体或程序块中声明的变量,其存储类别的默认形式是自动变量。当然,也可以使用关键字auto来进行显式声明,例如:
auto int x,y,z ;
auto double a =98.0;

静态变量(static修饰的变量)

静态变量的理解

由static修饰的变量都是静态变量
静态变量的特点是在程序开始运行前就为其分配了相应的存储空间,在程序的整个运行期间静态变量一直占用着这些存储空间,直到整个程序运行结束为止。因此,静态变量的生存期就是整个程序的运行期。

总的来说静态变量就两个作用,一个作业是延长变量的生存期,静态变量的生存期就是整个程序的运行期。
其次是静态变量可以共享数据,比如类的静态成员数据,可以被所有对象共享。

注意
在主函数的开始声明的局部变量也具有和整个程序运行期相同的生存期。

static修饰局部变量的作用

局部变量一般是动态变量,用static修饰之后就为静态局部变量。
静态局部变量的作用域仍为定义局部变量的函数或分程序,但其生存期扩大到整个程序的运行期。在声明静态变量的同时还声明了初值,则该初值也是在分配存储空间的同时设置的,以后在程序的运行期间不再重复设置。

静态局部变量的主要用途是保存函数的执行信息,以便下次进入该函数以后仍然可以继续使用。

static修饰全局变量的作用

全局变量一般就是静态变量,但是如果用static修饰全局变量,就可以将它的作用域限定在当前文件内部。

变量的存储类型与作用域的关系

全局变量都是静态变量,局部变量一般是动态变量,但是可以将局部变量变为静态局部变量。

寄存器变量

寄存器变量的理解

所谓寄存器变量,即为该变量分配的存储空间并不在内存储器中,而是CPU的某个寄存器中。
如果将某寄存器分配给一个变量,则由于该变量中的数据无须再去内存中存取,因此速度很快。但由于通常计算机中寄存器的数目很少,使用又很频繁,因此只有那些使用最多的变量才应该声明为寄存器变量。
尽管如此,C++规定,程序中只能定义整型寄存器变量(包括char型, int型和指针变量),而且程序员定义的寄存器变量并不一定都能分配到寄存器。C++的编译程序有权在寄存器不够分配时将一些或全部寄存器变量自动转换为一般变量。

注意:只有局部自动变量和形参可以作为寄存器变量,其它变量如全局变量、局部静态变量都不能作为寄存器变量出现。

寄存器变量的声明形式

寄存器变量的声明方法为在原来的变量声明语句之前加上 register,例如:
register int i,j;

外部变量

外部变量的理解

使用extern可以声明某一个变量为已定义外部变量,其格式如下:
extern <类型说明符><变量名表>;
此时是指这些变量是外部变量,而非本函数的同名局部变量。

外部变量的使用举例


注意
只有已经被定义的全局变量,才能在其他文件中被声明为外部变量。

auto自动推导变量的类型

auto自动推导变量的类型的理解

在C++11中,引入了auto关键字,可以用于自动推导变量的类型。

使用举例

1
2
3
4
5
6
7

vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();

//编译器会自动推导出it的类型为vector<int>::iterator。这样我们就不需要手动声明迭代器类型,简化了代码的书写。


使用auto的注意事项

使用auto自动推导变量类型的代价相对较小,但也有一些需要注意的问题:

  1. 代码可读性降低:使用auto可以让代码更简洁,但是也可能让代码难以阅读,特别是在变量命名不明确或代码行数过多的情况下。
  2. 类型错误难以发现:如果auto推导出的类型与预期类型不同,编译器可能会报错,但错误信息可能比较晦涩难懂,需要仔细分析才能找到问题。因此,建议在使用auto时仍然要给变量起一个明确的名称,并尽可能明确变量的类型。
  3. 可能会影响代码性能:虽然使用auto推导变量类型通常不会影响代码性能,但在一些特殊情况下,比如在循环中使用auto,可能会导致代码性能下降,因为每次迭代都需要进行类型推导。

总的来说,使用auto可以简化代码,减少类型错误,但需要注意代码可读性和性能问题。

运算符

自增运算符和自减运算符

自增运算符和自减运算符的用法

使自增运算符和自减运算符注意事项

1.作为运算符来说,“++”和“–”的优先级较高,高于所有算术运算符和逻辑运算符。

2.在使用这两个运算符时要注意它们的运算对象只能是变量,不能是其他表达式。例如, ( i +j)++就是一个错误的表达式。

=相关的运算符

跟=相关的运算符的作用

Sizeof运算符

Sizeof运算符的理解

sizeof 是一个关键字,它是一个编译时运算符,用于判断变量或数据类型的字节大小。
sizeof 运算符可用于获取常规数据类型、类、结构、共用体和其他用户自定义数据类型的大小。

语法

Sizeof(数据类型或变量)

特性

(0)sizeof是运算符,不是函数;

(1)sizeof不能求得void类型的长度;

(2)sizeof能求得void类型的指针的长度;

(3)sizeof能求得静态分配内存的数组的长度!
int a[10]; int n = sizeof(a);假设sizeof(int)等于4,则n= 10*4=40;
char ch[]="abc"; sizeof(ch);结果为4,注意字符串数组末尾有’\0’!

通常我们可以利用sizeof来计算数组中包含的元素个数,其做法是:
int n = sizeof(a)/sizeof(a[0]);
非常需要注意的是对函数的形参数组使用sizeof的情况。举例来说,假设有如下的函数:
void fun(int array[10])
{
         int n = sizeof(array);
}
你会觉得在fun内,n的值为多少呢?如果你回答40的话,那么我很遗憾的告诉你,你又错了。这里n等于4,事实上,不管形参是int的型数组,还是float型数组,或者其他任何用户自定义类型的数组,也不管数组包含多少个元素,这里的n都是4!为什么呢?原因是在函数参数传递时,数组被转化成指针了,或许你要问为什么要转化成指针,原因可以在很多书上找到,我简单说一下:假如直接传递整个数组的话,那么必然涉及到数组元素的拷贝(实参到形参的拷贝),当数组非常大时,这会导致函数执行效率极低!而只传递数组的地址(即指针)那么只需要拷贝4byte。

(5)sizeof不能对不完整的数组求长度;
file2.cpp包含如下几个语句:
int arrayA[];
int arrayB[10];
cout<<sizeof(arrayA)<<endl; //编译出错!!
cout<<sizeof(arrayB)<<endl;
在file2.cpp中第3条语句编译出错,而第4条语句正确,并且能输出40!为什么呢?原因就是sizeof(arrayA)试图求不完整数组的大小。这里的不完整的数组是指数组大小没有确定的数组!sizeof运算符的功能就是求某种对象的大小,然而声明: int arrayA[]只是告诉编译器arrayA是一个整型数组,但是并没告诉编译器它包含多少个元素,因此对file2.cpp中的sizeof来说它无法求出arrayA的大小,所以编译器干脆不让你通过编译。
那为什么sizeof(arrayB)又可以得到arraryB的大小呢?关键就在于在file2.cpp中其声明时使用int arrayB[10]明确地告诉编译器arrayB是一个包含10个元素的整型数组,因此大小是确定的。

(6)当表达式作为sizeof的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值!

(7)sizeof可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体!

(8)sizeof求得的结构体(及其对象)的大小并不等于各个数据成员对象的大小之和!

运算符的结合优先性

运算符优先级

运算符具有优先级和结合方向。一个表达式有不同运算符。则首先执行优先级别较高的运算。

运算符结合方向

某个运算符的结合方向,就是指这个运算符在和相同优先级的运算符一起构成的表达式的运算方向,就是这个运算符的结合方向。
注意:相同优先级的运算符的结合方向是一样的

例如表达式x* y/3 ,表达式里的运算符号都是相同优先级的。运算次序就是先计算x* y ,然后将其结果除以3,也就是说这个表达式的运算方向是从左到右,那么这一等级的运算符的结合方向就是从左到右。

也有些运算符的结合方向,是“自右至左”,例如赋值运算符,在表达式i=j=0中 ,表达式的计算顺序就是首先将0赋给变量j,然后再将表达式j=0.的值(仍为0)赋给变量i。

运算符的结合方向是限定在相同优先级的运算符之间,不同的优先级的运算符在一个表达式的时候则首先执行优先级别较高的运算。

运算符优先级和结合方向表



常用运算符和对应表达式

表达式的理解

表达式是由运算符将运算对象(如常数、变量和函数等)连接起来的具有合法语义的式子。









语句

表达式语句

表达式语句的理解

在表达式后面加一个分号;就构成了表达式语句。

条件表达式语句

条件表达格式(问号表达式语句)

<表达式1>?<表达式2>:<表达式3>
问号表达式的值的确定方法为:如果“表达式1”的值为非0值,则问号表达式的值就是“表达式2”的值;如果“表达式1”的值等于0,则问号表达式的值为“表达式3”的值。

举例说明

利用问号表达式可以简化某些选择结构的编程。
例如,分支语句:

逗号表达式语句

逗号表达格式

<表达式1>,<表达式2>,……,<表达式n>
在C++中可以使用逗号,将几个表达式连接起来,构成逗号表达式。
在执行程序时,按从左到右的顺序执行组成逗号表达式的各表达式,而将最后一个表达式(即表达式n)的值作为整个逗号表达式的值。

举例说明

typedef语句

typedef 语句的理解

typedef 语句(类型说明语句)的功能是为某个已有的数据类型定义一个新的同义字或别名。

typedef语句的格式

typedef <数据类型或数据类型名><新数据类型名> ;

举例说明

if语句

if语句的基本格式


在if 语句中用“表达式”的值来判断程序的流向,如果“表达式”的值不为0(即为true),表示条件成立,此时执行“语句1”;否则(即“表达式”的值等于0或false,表示条件不成立),执行“语句2”。

如果if和else下的实现,不能简单地用一条语句实现,可以使用由一对花括号{}括起来的程序段落代替“语句1”和“语句2”,即:

这种用花括号括起来的程序段落又称为分程序,它是C++中的一个重要概念。分程序是由花括号括起来的一组语句,其中也可以再嵌套新的分程序。
分程序是C++程序的基本单位之一,在分程序内部定义说明的数据变量的作用范围仅限于该分程序中,在分程序外面使用该变量是不合法的。

只有一个分支的if语句格式


只有一个分支的选择结构,可以使用不含else 部分的if语句表示。在这种情况下,如果<表达式>的值不为0,则执行“语句”或分程序,否则直接执行if 语句后面的语句。

多分支的if语句格式

if(表达式1) 语句1;
else if(表达式2) 语句2;
else if(表达式3) 语句3;
else if(表达式4) 语句4;
.
.
.
.
Else 语句n;
这里的语句都可以用花括号括起来的程序段落来代替。

switch语句

switch语句格式

switch语句用于实现多重分支,其格式如下:

其中, default模块也可省略。switch语句的执行过程是:首先计算“整型表达式”的值,然后将其结果与每一个case后面的数值常量依次进行比较,如果相等则执行该case模块中的语句,然后依次执行其后每一个case模块中的语句,无论整型表达式的值与这些case模块的进入值是否相同。如果需要在执行完本case模块以后就跳出switch 语句,可以在 case模块的最后加上一个break 语句,这样才能实现真正的多路选择。如果整型表达式的值与所有case模块的进入值无一相同,则执行default模块中的语句。。

while 语句

while语句的格式

1
2
3
4
5
6
7
8
while(<表达式>)
<循环体>
其中的“循环体”可以是一个语句,也可以是一个分程序,如下
while(<表达式>)
{
...
}

do - while语句格式

1
2
3
4
5
6
7
8
9
do
<循环体>
while(<表达式>)
其中的“循环体”可以是一个语句,也可以是一个分程序,如下
do
{

}while(<表达式>);

for语句

for语句的基础格式

1
2
3
4
5
for( 表达式1;表达式2;表达式3)
<循环体>

//与while语句的情况类似, for语句的循环体也可以是一条语句,或者一个分程序。

C++11 中的 range-based for 循环语法

1
2
3
4
5
for(i:v)
<循环体>
//for语句的循环体也可以是一条语句,或者一个分程序。该语法可以用于 STL 容器,也可以用于其他类似于容器的类型,例如数组,甚至是自定义类型,只要这些类型支持迭代器或使用 begin 和 end 函数返回其首尾迭代器即可。


for语句的举例说明

1
2
3
4
5
6
7
8
9
for(i=0;i<10;i++)
{
循环体;
}
该for语句用于重复十次循环。
第1次,i=0,然后判断i小于10为true,是则执行循环体,执行完循环体之后,执行i++操作。
第10次,i=9,然后判断i小于10为true,是则执行循环体,执行完循环体之后,执行i++操作,i为11;
第11次,i=11,然后判断i小于10为false,则循环结束。

C++11中的for循环语句举例说明

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

int main() {
vector<int> v = {1, 2, 3, 4, 5};

// 使用范围 for 循环遍历容器 v 中的每一个元素
for (auto i : v) {
cout << i << " ";
}

return 0;
}

//输出结果1 2 3 4 5
//在循环的每一次迭代中,变量 i 的值会被自动赋值为容器 v 中的下一个元素。在这个例子中,第一次迭代时,i 的值为 1,第二次迭代时,i 的值为 2,以此类推,直到遍历完容器 v 中的所有元素。


break语句和continue语句

break语句的格式

break;

break语句的理解

1
2
3
4
5
6
7
8
9
10
11
将该语句用在switch语句中,可以使程序流程跳出switch结构。
如果将break 用于循环语句,可以使流程立即跳出包含该break语句的各种循环语句,即提前结束循环,接着执行循环下面的语句。

在循环语句中使用的 break 语句一般应和if 语句配合使用,例如:
while(<条件1>)
{
...
if(<条件2>)break ;
...
}

continue语句格式

continue;

continue语句的理解

1
2
3
4
5
6
7
8
continue语句用于提前结束本轮循环,即跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判断,可用于while , do-while和for语句中。
continue语句的用法和break语句相似,均应和if语句配合使用,例如:
while(<条件1>)
{
...
if(<条件2>)break ;
...
}

break语句和continue语句区别

在循环中使用break语句和continue语句的区别是: break语句可提前结束整个循环的执行,不再进行条件判断,而 continue语句则只结束本次循环,而不终止整个循环过程。

goto语句

标号的格式

C++允许在语句前面放置一个标号,其一般格式如下:
<标号> :<语句> ;
标号的取名规则和变量名相同,即由下画线、字母和数字组成,第一个字符必须是字母或画线,例如:
ExitLoop : x = x + 1 ;
End: return x;

goto语句格式

goto语句的格式如下:
goto <标号>;
在语句前面加上标号主要是为了使用goto语句。
goto语句功能是改变语句执行顺序,转去执行前面有指定标号的语句,而不管其是否排在当前语句之后。C++的goto语句只能在本函数模块内部进行转移,不能从一个函数中转移到另一个函数中。

数组

数组的基础知识

数组的理解

在C++中,数组是一组具有相同数据类型的元素的集合。
他的作用就是可以用来存储同种类型的多个元素,并通过数组下标进行访问和操作。
通过使用数组,可以方便地管理和操作大量的数据,从而简化编程过程。
比如我们需要定义多个同种类型的元素时,一个个手动定义就很麻烦,这时候可以直接定义一个该类型的数组,通过这个数组,就可以存储管理该类型的多个元素

一维数组的定义语句格式

<类型><数组名>[<常量表达式>];
类型指出数组中元素的数据类型,可以是int , char , double等简单数据类型,也可以是结构体、类等复杂类型;
数组名是数组的标识,其构成规则同变量名;
常量表达式,它必须用方括号括起来,其值给出数组元素的个数,注意他必须是一个常量整型,而不能是变量。比如下面就是非法的

数组在内存的形式

数组在内存中是连续分配的一块区域,可以通过下标访问数组中的元素。以定义了个有10个元素的整型数组为例说明数组在内存的形式

int array [10];//有10个元素的整型数组
各元素通过不同的下标来区分,分别为array[ 0 ] ,array[ 1 ]array[2]、…、array[9]。同时,系统在内存中也为该数组分配了10个连续的存储空间,如下图

一维数组的定义时初始化

形式如下
<类型><数组名>[<常量表达式> ]= {<常量1 >,<常量2 > ,…} ;
在定义数组时给一维数组的每一个元素都提供初值,就可以不必指定数组大小。


double x[5] = {1.2, 3.2, - 3.5, 6.6, -4.1 } ;
等价于
double x[ ] = {1.2, 3.2, - 3.5, 6.6, -4.1 } ;

数组使用的注意事项

数组的使用方法和一般变量有所不同,C++不允许对数组进行聚集操作,即不能将整个数组作为一个单元来操作。
例如,假设数组a和 b是相同类型和大小的数组,如果想将数组a的值赋给b,下面的语句是错误的:
b=a;//不合法语句
要想实现这个功能,就必须对两个数组的每一个元素进行赋值,比如
a[0]=b[0]、a[1]=b[1]、.....

同样,为数组输入数据、输出数据、查找最大、最小元素等操作也都不能以数组整体为对象,而是需要对数组中每一个元素逐一进行处理,即要遍历整个数组。最常用的处理方法是通过循环处理数组中的元素。例如:
for( int i = 0; i < 10; i++ )
array[i] = 0;
不过在C++里,可以用输入流对象cin,cout,直接对字符数组进行输入或者输出。

二维数组

二维数组的定义格式

<类型><数组名>[ <常量表达式1>][<常量表达式2>]

二维数组的逻辑结构和存储结构

以一个二维数组为例
int matrix[3][4];//定义了一个3行4列的整型矩阵

逻辑结构

与一维数组相似,二维数组同样定义了类型相同的一组变量,这些变量也称为数组元素或下际变量。行,列下标值也是从0开始,依次加1。如上图所示, matrix[0][0]是矩阵matrix 中的第1行第1列元素。

存储结构

二维数组定义时初始化


注意
只能在定义时才能用这种方式进行初始化

多维数组

定义多维数组的一般形式

<类型><数组名>[<常量表达式1>][<常量表达式2>]…[<常量表达式n > ]
多维数组的用法和一维数组、二维数组一样。

定义三维数组的举例

例如:
float tri[2][3][3]//定义了一个2页3行3列的三维浮点型数组

三维数组其物理存储结构按自然顺序(即从下标序列对应的值从小到大顺序)在一片连续的内存中分配存储单元,例如 tri 在内存中以如下顺序存在:

字符数组

字符数组的作用

C++使用字符型数组存放字符串数据,并实现有关字符串的操作。
字符串包括一个结束符'\0'(即以NULL.结束),所以在计算用于存放字符串的数组的大小时要考虑到这一点。例如,如果要设计一个能够满足存放最大长度为80个字符的字符串的数组,其长度最少应为81。

字符数组的初始化

数组与指针

数组名的理解

实际上数组名本身就是一个常量指针,他指向数组首元素。
例如,对于数组 array[ 10 ],其数组名array就等效于地址&array [0]。因此,数组名array是一个指针,它永远指向array[0]

数组名与数组地址的区别

int a[10]为例,a则是数组名,表示是一个指针,指向的是数组首元素。&a则是数组的地址,他表示的是一个指针,指向整个数组,但是&aa的值是一样的,区别在于类型不一样,&a类型是int (*)[10],a的类型 int (*)

让a+1,则a+1指向数组第二个元素。而让&a+1,则系统会认为是整个数组的首地址加上整个数组的偏移(10个int变量),最终&a+1值就为指向数组尾元素的后一个元素的地址。

字符串

字符串的基础知识

字符串的理解

字符串就是一串由字符组成的连续序列。字符串在C++中,本质就是字符数组。

C风格字符串

C风格字符串,就是以空字符'\0'(NULL)结尾的字符数组,例如:"hello world"可以表示为字符数组{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'}

string类字符串对象

C++标准库提供的一种字符串类型string,string类定义的是字符串对象,可以用来代替C风格字符串,它未规定需要null ('\0')字作为字符串结尾,这是因为 string内部已经有一个变量来存储字符串的长度信息,而不需要依靠 null 字符来表示字符串的结尾。
不过编译器在实现时可能会自动在string字符串对象结尾加null ('\0')字符(根据编译器的不同,可能会加可能不会加)。

字符串的输入输出

字符串的cin输入

字符型数组的用法和普通数组基本相同;
而和普通数组不同的是,字符型数组允许聚集操作,而普通数组只能通过循环对逐个元素进行操作。例如:
char weekday[7];
cin >> weekday ;//将从键盘输入的字符串存入字符数组weekday中
注意:在上面的例子中,由于定义的字符数组维长为7,因此存储的字符不超过6个。如果用户输人的字符串长度大于6,系统会将输入的字符串顺序放在weekday后续的内存单元中,这会使后续的内存单元中的数据被破坏,造成严重错误。

在输入字符数组时还要注意,由于提取运算符>>会忽略所有空白字符,因此一旦遇到空白字符它就会停止读入数据到当前变量中。例如:
char name[ 20];
cin >>name;
当输人姓名Cong Zhen时,变量name中的字符串只有Cong,空格后的Zhen并没有被输人到变量name中。
由此可见,包含空格的字符串无法使用提取运算符>>来输入,解决的办法是使用输入流对象的get()成员函数。

string类的基础使用

string类的理解

string类属于标准库类。它是一个用来定义和处理字符串的类。通俗来讲,它所定义的对象就是字符串,而且string类里还定义了许多处理字符串的函数。
string本质封装了 char * ,管理这个字符串,是一个 char 型的容器。

使用string类所需头文件

#include <string>

使用 string字符串注意事项

需要注意的是,许多 C++ 标准库函数和 API 都假定字符串以 null 结尾,因此,当您将 sstring 对象传递给这些函数时,您需要确保在字符串末尾添加一个 null 字符,以免出现不可预料的错误。使用 std::string 类的成员函数 c_str() 来获取以 null 结尾的 C 风格字符串。例如:
std::string s = "Hello";
const char* cstr = s.c_str(); // 获取以 null 结尾的 C 风格字符串
在此示例中,cstr 指向的字符数组以 null 结尾,并且其内容与 s 中的字符串相同。

string类的定义

string类的字符串对象的使用方法与普通变量一样,也必须先定义才可以使用。其定义格式如下:
string <对象1 >,<对象2>,…;

例如:
string s1,s2;/定义对象s1和s2
string s3 ("world" );//定义s3同时初始化

string类字符串运算符

这些运算符允许在一般表达式中使用string对象,而不再需要调用诸如strepy( )或strcat( )之类的函数。同时,也可以在表达式中把string对象和一般以'\0'结束的字符串混在一起使用,例如可以把一个以'\0'结束的字符串赋给一个string 对象。

string字符串的使用举例



mystrcpy的使用

mystrcpy的使用

函数

函数的基础知识

函数的理解

函数的主要作用,就是可以封装程序语句。把一些程序语句封装在一个函数里,那么执行这个函数,就会顺序执行这一组程序语句。
当这些程序语句被封装在一个函数里,那么你想执行这些程序语句的时候,直接调用函数即可,就不用在重新去写这些程序。

函数的声明

声明的理解

C++规定,函数和变量一样,在使用之前,必须先进行说明。
函数声明没有函数体部分,且是用分号结束的,它向编译器提供了函数的名称、函数返回值类型和参数的个数’顺序及类型等信息,以便在对此函数的调用语句进行编译时据此进行检查而不会引起编译失败。

声明的形式

<函数值类型标识符> 函数名(<形式参数表>);
在函数声明中,参数的名字也可以省略,即可以不必指定参数列表的变量名,但必须指定每一个参数的数据类型。
比如原本函数的声明是int max(int a,int b);
可以写成int max ( int , int );

声明与定义的关系

函数声明没有函数体部分,且是用分号结束的,它向编译器提供了函数的名称、函数返回值类型和参数等信息。

函数定义则是对函数功能的确立,包括指定函数名、函数返回值类型和形参个数、顺序及类型、函数体等信息。

一般来说,函数的定义本身就可以当作声明。如果函数在使用的代码之前定义,那么就相对于已经声明了。如果想将函数定义放在调用它的main( )函数之后,或者将某函数的定义放在调用它的任意函数之后,就得在调用该函数的语句前对该函数进行声明。

函数先声明后定义的举例


尽管函数max( )的定义出现在对它的调用之后,但由于使用了函数声明,因此程序能够成功地通过编译。
其声明形式也可以写成int max ( int , int );

函数的定义

函数定义理解

函数必须先定义后才能使用。所谓定义函数,就是编写完成函数功能的程序块。

定义的形式

函数定义各参数说明

函数名
要定义的函数名字,其命名应符合C++对标识符的规定。在函数名后面必须有一对圆括号。

函数值类型
即调用该函数后所得到的函数值的类型。函数值是通过函数体内部的return 语句提供的。

形式参数
形式参数放在函数名后面的一对圆括号内,其作用表示将从主调函数中接收哪些类型的数据。
C++函数的形式参数表格式如下:
<类型><参数1 >,<类型><参数2 >,…,<类型><参数n>
如:
double grav( double m1 ,double m2 ,double distance)
将从主调函数中接收3个double类型的数据,分别赋给变量m1 ,m2和distance。
对于有些不带形式参数的函数,其函数名后面的括号为空,但一对圆括号不能省略。

函数体
函数体是由一对花括号括起来的语句序列(包括变量声明),这些语句实现函数的功能。在函数体中定义的变量只有在执行函数时才存在。

函数调用

函数调用的基础知识

函数调用的理解

函数调用的一般形式如下:
<函数名> (<实参表>)
其中,“实参表”是调用函数时所提供的实际参数值,这些参数值可以是常量、变量或者表达式。调用函数时提供给函数的实参应该与函数的形式参数表中参数的个数、位置和类型一一对应,称为“虚实结合”,此时形式参数从实参得到值。

函数的调用方式

在C+中,实参与形参有3种结合方式:值调用、引用调用和地址调用。

值调用

值调用的理解

值调用的特点是调用时实参仅将其值赋给了形参,因此在函数中对形参值的任何修改都不会影响到实参的值。
值调用的好处是减少了调用函数与被调用函数之间的数据依赖,增强了函数自身的独立性。

引用调用

引用的理解

引用是一种特殊类型的变量,可以认为它是另一个变量的别名。通过引用名访问变量与通过原变量名对变量进行访问的效果是一样的。

引用的声明形式

数据类型 &引用名=目标名

引用的定义举例

引用的作用

在实际应用中主要是作为函数的形式参数出现, 即将形参说明为引用。要将形参声明为引用,只要在参数名前加上引用运算符&即可。

用引用做形参的原因

如果进行普通的形参传值时,系统会给形参新分配一个内存空间,其中的内容和实参的内容一样,所以函数体内实质是对形参进行修改操作,对实参没有影响;如果形参是引用,这时的形参与实参所指向的内存是一样的,系统不会再重新分配空间,能保证参数传递中不产生副本,提高传递的效率,解决大块数据或对象的传递效率和空间不如意的问题

常引用的格式

const <类型说明符> &<引用名>
如果在说明引用时用const进行修饰,就构成了常引用,这样引用就无法被修改。即以下语句是错误的:
int a=5;
const int &b = a;
b = 12;//b是a的常引用,不能修改a中的内容

常引用做形参作用

用形参作为参数一个局限性,就是如果形参被修改了,实参也会被修改,如果想让引用不被修改,就在引用前加上const,变成常引用,常引用可以作为函数的形参(常参数),来实现函数体内只能进行对变量的读取而不能改写的操作。

引用使用的注意事项

1.创建引用的同时必须初始化引用。
2.一旦初始化引用,就不能再改变引用的关系。
3.不能有NULL引用(空引用),引用必须与合法的存储单元相关联。
4.引用的类型和变量的类型必须相同。

引用调用的举例

swap函数

主函数

结果

地址调用

指针作为函数的参数理解

当以指针作为参数时,在函数调用过程中实参将地址值传递给形参,即使实参和形参指针变量指向同一个内存地址。
这样,对形参指针所指变量值的改变也同样影响着实参指针所指向的变量的值,即通过使实参与形参指针指向共同的内存空间,达到了参数双向传递的目的。

数组作为函数的参数的理解

如果使用数组作为函数的参数,则实参和形参都应该是类型相同的数组名。与普通变量做参数不同的是,如果在函数中对形参数组的值进行改变,将会使对应的实参数组元素的值也发生改变。
这是因为使用数组名传递数据时,并不是简单的值调用方式,而是传递了数组所在的地址。这样,在子函数中对数组的操作,实际上就是对主调函数中的数组本身进行操作,这种实参与形参的结合方式就是地址调用。

带有默认参数的函数

带有默认参数的函数的理解

C++允许在函数声明或函数定义中为参数预赋一个或多个默认值,这样的函数就叫做带有默认参数的函数。
在调用带有默认参数的函数时,如果为相应参数指定了参数值,则参数将使用该值;否则参数使用其默认值。

带有默认参数的函数使用举例

例如,
某函数的声明为:double func( double x,double y, int n = 1000 );
则参数n带有默认参数值。

如果以a=func( b,c);
的方式调用该函数,则参数n取其默认值1000,

而如果以a = func( b,c,2000 ) ;
的方式调用该函数,则参数n的值为2000。

使用带有默认参数的函数的注意事项

1.所有的默认参数均需放在参数表的最后。

2.如果一个函数有两个以上的默认参数,则在调用时可省略从后向前的若干个连续的参数值。例如,对于函数
void func( int x , int n1 = 1 , int n2 =2) ;
若使用func(5,4);的方式调用该函数,则x的值为5 ,n1 的值为4 , n2的值为2。

3.默认参数的声明必须出现在函数调用之前,
即如果存在函数声明,则参数的默认值应在函数声明中指定,否则在函数定义中指定。

4.如果函数声明中已给出了参数的默认值,则在函数定义中不得重复指定,即使所指定的默认值完全相同也不行。

无参函数

无参函数的理解

在C++中,函数的参数表可以为空,例如下面函数的声明
void func();
说明函数func()既不需要参数,其调用方法如:func();

函数返回值

返回值的理解

函数是什么类型,调用该函数后就会返回这个类型的数值。函数值是通过函数体内部的return 语句提供的。

return的理解

一是使流程返回调用它的函数(即主调函数),说明该函数一次执行终结,同时释放掉在调用期间所分配的变量单元;
二是把函数值送到调用表达式中。
在编写函数时要注意保持return语句提供的函数值的类型与函数说明中的函数值类型一致,否则会出现错误。

return的形式

return <表达式>;

void函数的返回值

如果要定义的函数确实没有返回值,可以使用说明符void。
此时函数类型是void类型,可以不用有返回值,因此也可以省略return语句。
不过void也是可以使用加return的,但是其后面不能加任何数值 表示函数到此为止

注意

有些函数可能没有函数值,或者其函数值对调用者来说是不重要的,调用该函数实际上是为了得到运行该函数内部程序段的其它效果。

库函数和标准库函数

库函数和函数库的理解

库函数是指提供特定功能的函数,这些函数可以由编程人员编写或者由第三方库提供。不同的函数库一般是用来针对特定任务,如数学库、图形库、网络库等。

标准库函数和标准库

标准库函数,是C++编程语言为了方便程序员编程,预先编制的函数,
用户不用定义也不用声明就可以直接使用。不同功能标准库函数,被分类放在不同的标准库中(头文件)。

标准库函数和库函数的区别

标准库函数也可以被称为库函数,但库函数并不一定是指标准库函数。它们是两个不同的概念,一个是特定功能的函数库,另一个是编程语言提供的函数库。

库函数的使用

由于C++软件包将不同功能的库函数的函数声明分别写在不同的头文件中,因此用户在使用某一库函数前,必须用include预处理指令给出该函数的原型所在头文件的文件名。
例如,欲使用库函数 sqrt ( ),由于该函数的原型声明在头文件cmath中,因此必须在程序中调用该函数前应加入以下语句:
#include <cmath>

递归函数

递归函数的理解

如果一个函数直接或者间接的调用自身,那么这个函数就是递归函数。
递归函数分为直接递归函数,和间接递归函数。

直接递归函数的理解

也就是说,在定义一个函数时,如果在其函数体内直接包含了调用该函数本身的语句,则这种调用是直接递归调用方法,该函数是直接递归函数;

间接递归函数的理解

如果函数在其函数体中间接包含对自己的调用,例如函数A调用了函数B,函数B又调用了函数A,则函数A称为间接递归函数。

递归函数的使用举例


函数重载

函数重载的理解

所谓函数重载,即一组参数和返回值不同的函数共用一个函数名。

函数重载的作用

在C++标准函数库中,有3个功能相似的函数:
intabs( int ) ;
double fabs( double) ;
long labs( long);
同是求某数的绝对值,要用不同的函数实现,不但增加了程序员的记忆难度,而且也容易出错。此时,C++中的函数重载可将求三个函数用同一函数名字调用。

原理

由函数的定义可知,函数具有两个要素:参数与返回值。很明显,如果同名函数仅仅是返回值类型不同,在进行函数的调用时,编译器根本无法区分不同的函数。因此,只能靠参数而不能靠返回值类型的不同来区分同名的重载函数。

由此可知,编译器是根据函数参数的不同(包括类型,个数和顺序)来确定调用哪一个函数的。因此,重载函数之间必须在参数的类型或个数方面有所不同。只有返回值类型不同的几个函数不能重载。

举例


输出结果

说明
本例中定义了3个同名的函数 abs( ) ,分别为求整型量:实型量和长整型量绝对值的函数。在 main( )函数中分别调用这3个函数求x1 ,x2、x3的绝对值。

static修饰函数(静态函数)

static修饰函数的作用

static关键字的作用是将函数的作用域限定在当前文件内部,从而使函数对其他文件不可见,也就是说,它只能被同一文件内的其他函数所调用,而不能被其他文件所调用。
使用static关键字修饰函数还有一个作用,就是将函数变成静态函数,使得该函数的生命周期与程序运行期间相同,不会随着函数的调用而被创建和销毁。这样可以提高程序的执行效率,因为不需要频繁地创建和销毁函数所占用的栈空间。

内联函数(inline关键字)

内联函数的理解

函数的定义开头用inline关键字修饰,该函数就变成内联函数。
如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
将函数体的代码直接插入到函数调用处来节省调用函数的时间开销,这一过程叫做内联函数的扩展。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
内联函数实际上是一种用空间换时间的方案

内联函数和普通函数的区别

普通函数的调用是要转去执行被调用函数的函数体,执行完成后再转回调用函数中,执行其后语句.

内联函数的调用是在函数的调用点,用内联函数体的代码来替换,这样将会节省调用开销,提高运行速度。 

内联函数与带参数的宏定义的代码效率是一样的,但是内联函数要优于宏定义,因为内联函数遵循函数的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关上内联扩展,将与一般函数一样进行调用,调试比较方便。 

内联函数的代码编译后会直接放在调用点的函数体内, 从而使得代码增大, 但是效率提高了(减少了跳转, 参数传递以及保存调用函数寄存器状态的过程).

对外联函数的调用会在调用点生成一个调用指令(在X86中是call), 函数本身不会被放在调用者的函数体内, 所以代码减小, 但效率较低.
所以一般只有当函数体较小的情况下才声明为内联函数

使用举例


inline关键字的理解

用inline可以将一些函数声明为内联函数,
但是这些函数不一定会被编译器内联,仅当编译器的成本收益分析显示有价值时,它才会进行内联。
比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数).

_forceinline关键字的理解

用inline声明函数为内联函数的时候,这些函数不一定会被编译器内联,编译器会判断情况来进行内联。 而如果使用_forceinline关键字来声明函数,则是可以强制让函数为内联,这时候判断函数是否内联,则不是编译器,而是程序员自己。
不过有些情况下,哪怕使用_forceinline关键字,也无法对某些函数进行内联。

使用内联函数的注意事项

带参数的main函数

带参数的main函数的理解

在本书前面出现过的所有例子中 , main()函数都是不带参数的。而实际上, main()函数也是可以带参数的,其函数原型声明为:
int main( int argc,char * argv[ ])

其中,第一个整型参数指明在以命令行方式执行本程序时所带的参数个数(包括程序名本身,故arge的值至少为1);
第二个参数为一个字符型指针数组,其中第一个下标变量argv[0]指向本程序名,下标变量argv[1 ] , argv[2]…等分别指向命令行传递给程序的各个参数的字符串。

注意
其中main函数的形参不一定命名为argc和argc,可以是其他名字,只不过人们习惯用这两个名字。

使用带参数的main函数的举例

编写的test.cpp文件

假设本例生成的执行程序文件名为test.exe,且存放在C盘根目录下,则可在命令行方式下输入
C: \> test Zhang3
test为可执行文件名(可执行文件名包括盘符路径,但是在C盘根目录下,可直接用test) zhang3为传给main函数的参数, 此时main函数a=2,ar[0]指向test字符串首字符地址,ar[1]指向zhang3字符串首字符地址
来运行这个程序,程序的输出结果为:
Hello,Zhang3. Welcome to the world of C ++ !

指针

地址

地址的理解

从拓扑结构上来说,计算机的内存储器(简称内存)就像一个巨大的一维数组,每个数组元素就是一个存储单元。就像数组中的每个元素都有一个下标一样,每个内存单元都有一个编号,称为地址,它可用一个无符号整数来表示。计算机就是通过这种地址编号的方式来管理内存数据读/写定位的。

运行程序的存放位置

内存是程序活动的基本场地。在运行一个程序时,程序本身及其所用到的数据都要放在内存中,例如程序、函数、变量、常数﹑数组和对象等。

数据的地址表示

凡是存放在内存中的程序和数据都有一个地址,一般用它们所占用的存储单元中的第一个存储单元的地址表示。
可以理解为,数据的存储可能需要多个存储单元来存储(不一定只用一个存储单元,),但是数据地址的表示只需要一个存储单元的地址表示。

数据的访问方式

在编写程序时,一般是通过名字来使用一个变量或者调用某个函数,而变量和函数的名字与其实际存储地址之间的转换由编译程序自动完成这种方式称为直接存取访问方式。
同时,C++也允许采用间接访问方式,即编程者通过操作变量、数组或者函数的地址来达到处理数据的目的。在很多情况下用这种方法可以提高程序的运行效率。

数据的访问原理

访问数据,就得知道数据的首地址和地址的长度。通过首地址和地址长度,就能得到数据所占用的所有存储单元,然后就可以访问这个数据。
当我们去访问数据的时候,我们只给了系统数据的首地址,那么系统是怎么判断要获取的数据的地址长度?系统会通过我们给的数据地址的类型,得知存储的数据的地址长度,再通过我们给的数据的首地址,从而得知数据的所在的整个存储单元,然后就可以访问整个数据。

数据的地址获取

  1. 变量的地址可以使用地址运算符&求得。例如,&x表示变量x的地址。
  2. 数组的地址,即数组第一个元素的地址,可以直接用数组名表示。
  3. 函数的地址用函数名表示。(实际上,函数名只是表示函数的首地址,而不是真正的函数的地址。函数名和函数地址两者的值都是函数的首地址,但是类型不一样。真正的函数地址得用&+函数名获得)

指针

指针的理解

指针有两种理解。
一,某个变量的内存地址称为该变量的指针。
二,用以表示(或存储)不同指针值(亦即内存单元地址值)的变量就是指针变量,简称指针。指针也是一种数据类型。

指针变量的定义形式

定义的形式
数据形式 *指针变量名

定义形式的说明

  1. "*“不是指针变量名,表示这里说明的是一个指针类型的变量。
  2. 数据形式 ,它是指针要指向的变量的类型。( 可以把"*“和数据形式看成是一个整体,比如int *i,指针i的数据类型是int*类型。)
  3. 指针变量的值是指针所指向的变量在内存中所处的地址。
  4. 指针的变量名是标识指针变量的名称,其命名规则与一般变量相同。

举例

int * ptr ;
定义了一个指向 int类型的指针变量,其名称为ptr,专门用来存放整型数据的地址

悬挂指针的理解

悬挂指针(Dangling Pointer)是指指向已被释放或无效的内存地址的指针。

野指针的理解

野指针(Wild Pointer)是指没有被正确初始化或者指向无效对象的指针。野指针可能包含任意值,指向任意的内存地址,包括已经被释放的内存、未分配的内存区域或者无效的对象。

指针运算

*运算符和&运算符

*运算符的理解

*称为指针运算符,*出现在说明语句中和执行语句中时其含义是不同的。

*出现在说明语句中,在被说明的变量之前时,表示说明的是指针,例如:
int * ptr ;//说明ptr是一个int型指针
ptr = &x ;//取变量x的地址

*出现在执行语句中或说明语句的初值表达式中时,表示访问指针所指向变量的值,例如:
y=* ptr;//将指针ptr所指向的值赋给变量y

&运算符的理解

&称为取地址运算符。&出现在说明语句中和执行语句中时其含义是不同的。

&在给变量赋初值时出现在赋值号右边或在执行语句中作为一元运算符出现时,表示取变量的地址,如:
ptr = &x ;//取变量x的地址

&出现在变量说明语句中,位于被说明变量左边时,表示说明的是引用,例如:
int &ref ;//说明一个int型的引用ref

指针变量的算术运算

指针变量算术运算分类

指针变量只有和加减法相关的算术运算,大概有三种
1自增++、自减–运算。
2加、减整型数据。
3指向同一个数组的不同数组元素的指针之间的减法。

不同类型的指针的加减法的区别

指针变量的加、减法运算可以完成指针移动。对不同的指针变量类型,移动的单位长度有所不同。移动单位长度的大小取决于指针变量的类型和指向的数据类型的大小。
指针变量的值存储的是它所指向的数据的首地址,而不是整个数据所占的内存空间。因此,对于不同类型的指针变量,它们的值都只是所指向数据的首地址,不过他们的移动单位不同。
例如,对于整型指针,移动单位长度为整型数据类型的大小,即4个字节。而对于双精度指针,移动单位长度为双精度数据类型的大小,即8个字节。

因此,对于整型指针p而言,p–操作会使指针p的值减去整型数据类型的大小,即4个字节,指向前一个整型数据单元的首地址。同理,p++操作会使指针p的值加上整型数据类型的大小,指向后一个整型数据单元的首地址。

使用指针变量的算术运算的注意事项

1.对于指向单个变量和函数的指针进行算术运算是没有意义的。
因为指针的加减操作的本质是在指针的基础上移动一定的步长,而这个步长的大小取决于指针所指向的数据类型的大小。对于单个变量或函数来说,它们在内存中只占有一个固定的地址,没有其他的元素需要移动,因此对指向它们的指针进行算术运算没有意义。
但是对于数组来说,一个数组内的各元素的相对位置总是固定的,因此对指向数组元素的指针使用算术运算是有意义的可以根据指针移动的距离来访问数组中的不同元素。

2.对指针变量进行下列算术运算毫无意义:指针间相乘或相除、两个指针相加、指针与浮点型数的加、减等。

指针变量的比较运算

指针变量的比较运算的意义

在关系表达式中允许指针的比较运算,但要注意这种运算对程序设计是否有意义。
1.一般来说,指针的比较常用于两个或两个以上指针变量都指向同一个公共数据对象的情况,例如同一个数组中各数组元素的指针之间的比较等。
2.任何指针与空指针(NULL)的比较在程序设计中都是必要的,但类型不同的指针之间的比较一般没有意义。

指针变量的下标运算

指针变量下标运算的理解

C++提供了指针变量的下标运算[],其形式类似于一维数组元素的下标访问形式。例在声明了指针变量
double x,a[100],*ptr = a;
之后,也可以使用
x= ptr[10];//此时就是把a数组的第十个元素,赋值给x。
但是注意ptr本质不是数组,而是指向数组首元素的指针,ptr[10];这个语句就相当于( ptr +10 ) ;语句一样。

指针初始化

指针初始化的原因

声明一个指针变量后,如果没有对其赋初值,则它的值(即它所指向的内存位置)是不确定的。
此时,指针所指向的内存单元有可能存放着重要的数据或代码,如果盲目地访问,就可能会破坏数据或导致系统出现故障,因此不赋值而直接对指针所指向的内存写人数据是极其危险的。
为避免上述错误,需要在声明后对指针变量进行“初始化”。

普通指针的初始化

定义指针变量的同时,赋予该指针变量初值,其一般形式如下:
数据类型标识符*指针变量名=初始地址值;
例如:
int i ;
int*ptr = &i ;

空指针

空指针的理解

实际上,在编程实践中,程序员经常使用以下的初始化语句:
int *ptr = NULL;
在此,指针 ptr被初始化为NULL,即空指针。NULL是一个在头文件iostream中定义的符号化常量,将指针初始化为NULL就等于将指针初始化为0。值为NULL的指针不指向任何变量。
在定义指针时将指针初始化为NULL是一个很好的编程习惯,这样做可以防止该指针变量指向某一个未知的内存区域而产生难以预料的错误。

指向数组的指针

指向数组的创建指针的举例

1
2
3
4
5
6
int a[10];
int (*p)[10] = &a;
//则p是指针,指向a数组,类型为int (*)[10]。这里的括号是必要的,因为[]运算符的优先级比*运算符高。
如果不加括号,编译器会将上面的声明解释为int *p[10],此时p是指针数组,数组的每一个元素都是指针。
由于数组中的元素在内存中是连续排列存放的,因此任何能由数组下标完成的操作都可由指针来实现,比如a[0]表示数组的第一个元素,而(*p)[0]也表示数组的第一个元素

指向数组的指针和指向数组名的指针的区别

以下面代码为例
int a[10];
int (*p1)[10] = &a;
int *p2=a
p1是指针,指向a数组,类型为int (*)[10]
可以使用(*p1)[i]来访问数组元素,其中i表示元素的下标。而p1++将指向下一个数组,因为一个int (*)[10]类型的指针移动时会跨越整个数组。
(对指向数组的指针进行解引用,就可以看成是数组名)

p2是指针,指向a[0]元素,类型为int *
可以使用p2[i]来访问数组元素,其中i表示元素的下标。而p2++将指向数组中的下一个元素,因为一个int *类型的指针移动时只会跨越一个元素。

指向字符串的指针

指向C风格字符串的指针

我们知道C风格字符串本质就是以空字符'\0'(NULL)结尾的字符数组。而编译器只需要知道字符数组的数组名,也就是字符数组的首个元素的地址,就可以获取整个字符串,这是因为,编译器在处理字符串时,会从字符数组的首地址开始逐个读取字符,直到读到 '\0' 字符为止,就可以确定字符串的长度。
那么当我们要用指针就指向整个字符串,只需定义一个字符指针即可,让字符指针去指向数组首元素的地址。

使用指向C风格字符串的指针的注意事项

在C++中,给一个指针变量赋值一个字符串常量,例如char *c = "ABC";是不安全的。实际上是将指针 c 指向了一个字符串常量"ABC"所在的内存空间。这个字符串常量是存储在只读内存区域(例如代码段或全局数据区)的,因此我们不能通过指针 c 来修改这个字符串的内容。
因此在 C++ 中,这段代码可以被编译通过,但是在 C++11 标准之后,这样的代码被视为不安全的

指向字符数组的指针的使用

设有指针ptr . qtr以及字符型数组 string,代码如下
char * ptr,*. qtr;
char string[6] = "Big" ;
int len = strlen( string) ;
ptr = string;
qtr = ptr + len;
他们在内存中的关系如下图


从图中可以看出,指针ptr 指向数组string 的第一个元素,其内容就是该元素的地址0x0012EF70。
现在,如果执行运算
ptr ++ ;
即在指针变量ptr原来的值上再加1,使其变为0x0012EF71。可以看出,这正是数组中第二个元素的地址,即指针现在改为指向数组中的第二个元素。

如果执行运算
ptr += 3;
则ptr的值由0x0012EF70变为0x0012EF73,即指向数组string的第四个元素。由此可以看出,在指针变量上加上一个常数,相当于改变了其中存储的地址值,即改变了指针指向的数组元素。同样,也可以从指针变量存储的地址值上减去一个常数,此时指针向前移动若干个元素

在图中,令指针变量qtr指向字符串的结束标志为0(即数组string 的第四个元素)。以下语句可求出这两个指针的差:
3 = qtr - ptr;
可以看出,这正是字符串string的长度(不含字符串结束符)。

由于在C++中每个变量﹑数组和函数的具体地址和相对顺序是由编译连接程序确定的(局部变量是动态分配的) 。
在编写程序时无法知道其确切的地址和相对顺序,因此对于指向单个变量和函数的指针进行这样的运算是没有意义的。
但是无论怎样分配,一个数组内的各元素的相对位置总是固定的,因此对数组元素的引用除了使用下标以外,还可以通过使用指针运算来实现,这是C程序设计的一大特点。

指向指针的指针

指向指针的指针的理解

指针也是变量,当然也有地址。指针的地址也可以使用地址运算符“&”求出,也可以存储在某种变量中。能够存放指针地址的变量当然也是指针,是“指向指针的指针”。

指向指针的指针的声明形式

<数据类型> **<指针变量名>;

指向指针的使用举例

int x = 2 ;
int* xp,**xpp;
xp = &x;
xpp = &xp;

内存分配图

xpp是指向xp的指针,即存储着xp的地址,则*xpp则表示xpp所指向的存储空间的值,也就是说*xpp表示xpp所存储的地址所存储的值,即xp的值。

xp是指向x的指针,即存储着x的地址,*xp表示xp所指向的存储空间的值,也就是说*xp表示xp所存储的地址所存储的的值,即x的值2。

*xpp表示所指向的存储空间xp的值,xp存储着x的地址,**xpp就表示所存储的x的地址的值,也就是2

指针数组

指针数组的理解

指针数组也是数组,但它和一般数组不同,其数组元素不是一般的数据类型,而是指针,即内存单元的地址。这些指针必须指向同一种数据类型的变量。

指针数组的声明方式

指针数组的声明方式和普通数组的声明方式类似,在数组名后加上维长说明即可。
声明一维指针数组的语法形式如下:
数据类型 *数组名[常量表达式];
其中,常量表达式指出数组元素的个数,数据类型确定每个元素指针的类型,数组名是指针数组的名称,同时也是这个数组首元素的首地址。

指针数组的声明举例

声明一个一维指针数组,10个数组元素,均为指向字符类型的指针:
char * ptr[10];

当然,也可以声明二维以至多维指针数组,例如:
int * index[10][ 2];

指针数组的初始化

动态存储(new语句)

静态存储的理解

一般来说,程序中使用的变量和数组的类型、数目和大小是在编写程序时由程序员来确定的,因此在程序运行时这些数据占用的存储空间数也是一定的,这种存储分配方法被称为静态存储分配。
静态存储分配的缺点是程序无法在运行时根据具体情况(如用户的输人)灵活调整存储分配情况。
例如,无法根据用户的输入决定程序能够处理的数组的大小,如下的代码是错误的。
int a;
cin>>a;
int b[a];

动态存储的理解

C++的动态存储就可以解决静态存储问题。
C++中的动态存储是一种在程序运行时动态分配内存的方法,这种内存分配方式不需要在程序编写时预先知道所需内存大小。动态存储允许程序根据需要分配和释放内存,可以使程序更加灵活和高效。

就是一般情况下,我们的数组大小是在我们编程的时候设置好的。而动态存储,可以让我们在运行程序的时候,根据不同的情况,随意调整数组的大小

在C++中,使用new运算符可以动态分配内存,返回指向新分配的内存的指针。

new动态申请内存的语法

申请变量的语法
<指针> = new<类型>;
或者
<指针> = new<类型>(初值);
new运算符从堆(管理内存中的空闲存储块)中分配一块与“类型”相适应的存储空间,如果分配成功,则将其首地址存入“指针”,否则置“指针”的值为NULL(空指针值,即0)。“初值”用于为分配好的变量置初值。

申请数组的语法
<指针> = new<类型>[<元素数>];

delete释放先前申请到的存储块语法

释放申请到的变量
delete<指针>;
其中,“指针”中应为先前分配的存储块的地址。

释放申请到的数组
delete []<指针>;

基础使用举例1

1
2
3
4
5
6
7
8
9
10
11
12
//分配了一个大小为10个整数的数组
int* myArray = new int[10];

//使用“delete”运算符释放该内存。
delete[] myArray;

//使用new申请int类型内存,并在内存里赋予初值5
int x,*ptr = new int( 5 );
//x的值为5
x=* ptr;


二维数组动态存储和释放使用举例

使用动态存储创建一维数组,并且让数组里的元素都为0

1
2
// 方法三:使用花括号初始化列表(C++11特性)将数组所有元素初始化为0
int* myArray = new int[N](); // 注意:这里必须加上括号

在使用动态存储时,需要注意以下几点:

  1. 动态分配的内存不会像栈内存一样自动释放,需要手动释放,否则会导致内存泄漏。
  2. 内存分配失败可能会导致“bad_alloc”异常,需要在代码中进行异常处理。
  3. 动态分配的内存可以使用指针进行访问,但需要注意指针的生命周期和有效性。
  4. 用这种方法创建的动态数组,在C++没有对应的函数来求其数组长度。比如sizeof只能求静态数组的长度,不能求动态数组的长度。

函数的地址和指针

函数地址的理解

程序只有装人内存以后才能运行。函数本身作为一段程序,其代码也在内存中占有一片存储区域,这些代码中的第一个代码所在的内存地址称为首地址。
函数的地址就是用函数的首地址表示,但是注意函数的首地址不代表就是函数的地址,两者的值一样,但是类型不一样。
首地址是函数的入口地址。主函数在调用子函数时,就是让程序转移到函数的入口地址开始执行。

指向函数的指针的理解

所谓指向函数的指针,就是指针的值为该函数的入口地址,指针的类型,与函数地址的类型一致。

指向函数的指针变量的定义形式

<函数返回值类型说明符>(*<指针变量名>)(<参数说明表>);

指向函数指针定义举例

int(*p)( ); //p为指向返回值为整型的函数的指针
float(*q)( float , int ); //q为指向返回值为浮点型,且有两个参数的函数的指针

函数的赋值

由于函数名与数组名类似,都表示该函数的入口地址,因此可以直接把函数名赋给指向函数的指针变量。

注意,函数名虽然表示的是函数的首地址,但函数名不是函数的地址,函数的地址则是&函数名。
函数名和函数的地址两者的值是一样的,但是意义不一样,类型也不一样。假设有一个函数 void test()。test是函数的首地址,它的类型是void (),&test表示一个指向函数test这个对象的地址, 它的类型是void (*)(),他们所代表的值一样,都是函数的首地址。但类型不一样。(指向函数的指针,和函数的地址两者的数据类型是一样的)
赋值的时候,可以用函数名赋值给函数的指针,是因为赋值的时候,C++会自动进行类型的转换,函数名所表示的数据地址会转换为函数地址的数据类型。

指针变量调用函数举例

double ( *func)(double) = cos ;
double y,x;
x= 3.14159;
y=(* func ) ( x ) ;

注意

  1. 在定义指向函数的指针变量时,指针变量名前后的圆括号不能缺少。例如:
    int* func( ); //返回地址的函数
    int (*func)( ) ; //指向函数的指针
  2. 前者定义了一个函数,其返回值为指向整型的指针;而后者定义了一个指向返回值为整型的函数的指针变量,意义完全不同。

void类型的指针

void类型指针的理解

C++规定,可以说明指向void类型的指针。指向void类型的指针是通用型的指针,可以指向任何类型的变量。可以直接对void型指针赋值或将其与NULL进行比较,但是在求指针变量的内容,或者进行指针运算之前必须对其进行强制类型转换。

举例

指针常量和常量指针

指针常量的理解

指针常量是指指针本身是常量,即指针变量的值(内存地址)不能被改变,但是可以通过该指针访问和修改所指向的数据。

指针常量的举例

1
2
3
4
5
int value = 10;
int* const ptr = &value; // ptr 是指针常量,指向 value
*ptr = 20; // 通过指针修改 value 的值

在上面的例子中,ptr 是一个指针常量,它的值,即所存储的地址不能被改变,但是可以通过 *ptr 来修改所指向的 value 的值。

指针常量的举例2

char str2[ ] = " A constant pointer" ;
char * const qtr = str2 ;
定义了一个指针常量qtr。在这种情况下,指针本身不能修改,但其指向的对象并非常量,允许修改。

常量指针的理解

常量指针是指指针所指向的数据是常量,即指针指向的数据不能被修改,但是指针本身可以指向其他的数据。

常量指针的举例

1
2
3
const int value = 10;
const int* ptr = &value; // ptr 是常量指针,指向常量 value
在上面的例子中,ptr 是一个常量指针,它指向一个常量 value,因此不能通过 *ptr 来修改所指向的数据。

常量指针的举例2

char str1 [ ] = "Point to constant string" ;
const char * ptr = str1 ;
表示定义了一个指针ptr,它指向一个常数字符串。

因此,运算
*ptr = 'Q';
是非法的,因为该字符串为常量。

但指针ptr本身为变量,可以修改。例如
ptr ++ ;
合法.

总结

1
2
const int* A;//此时A所指向的对象可以看成是常量,因此*A不能修改,而A可以变。
int* const A; //地址A可以看成是常量,A不可变,A所指向的对象是可以变的。

const修饰符的常用用法

1
2
3
4
5
实际上,修饰符const多用于修饰函数的指针或引用参数,以防止在编程中无意识地改变其值。例如:
double func1 ( const double * x );
double func2 ( const double &x );
如果在编写以上函数的代码时不小心改变了指针对象或引用对象的值,则会引起编译错误。

结构体类型

结构体的基础知识

结构体的理解

结构体是由不同类型的数据组成的集合体,它包含多种成员。在使用结构体之前得先定义结构体

结构体的定义

1
2
3
4
struct <结构体类型名>
{
<结构体类型的成员变量说明语句表>
}

C的结构体和C++结构体的区别

实际上,结构体类型是C++从C语言中继承下来的内容。C语言的结构体类型比较简单,只有成员变量。而C++的结构体类型和类(将在第9章介绍)一样,可以有数据成员,也可以有成员函数﹑构造函数和析构函数。
C++中,使用结构体类型主要是为了兼容一些从C语言继承下来的函数厍。
虽然C++的结构体与类十分相似,但大多数程序员还是习惯于使用类似于C语言的结构,即结构体中成员的定义只包含数据,而不包含成员函数。

C++的结构体和类的区别

C++的结构体类型的定义和使用方法与类非常相似。在C++中,结构体类型与类的唯一区别是,如果不明确说明,则类的成员均为私有,而结构体类型的成员均为公有。

结构体的使用举例

即一个日期类型的变量有3个成员变量:年份( da_year)、月份( da_mon)和日(da_day)。
在定义了结构体类型之后,就可以声明该类型的变量,结构体变量的声明方法和其他类型的变量一样,例如变量说明语句:
Date yesterday, today, tomorrow ;
就定义了3个日期类型的变量:yesterday,today 和 tomorrow ,这些变量具有数据处理对象包含的所有数据,可以与简单类型的变量一样作为函数的参数或返回值,或构造结构体类型的数组。

typdef声明结构体

在结构体定义中,用typedef和不用的区别

1
2
3
4
5
6
7
8
9
10
11
12
struct Student
{
int no;
char name[12];
}stu1;//stu1是一个变量

typedef struct Student2
{
int no;
char name[12];
}stu2;//stu2是一个结构体类型,即stu2是Student2的别名

结构体变量初始化和使用

定义结构体变量时赋值

例如
Date yesterday = {2008,1,28} ;
两个相同类型的结构体变量之间可以互相赋值。

对结构体类型变量的成员变量的引用方法为

<结构体类型变量名>.<成员变量名>
例如:
today. da_year = 2008 ;
today. da_mon = 1 ;
today. da_day = 29;

结构体的输入输出

和数组一样,不能将结构体变量作为一个整体输入或者输出。
只能以结构体的成员作为基本变量,一次输入或输出结构变量中的一个成员。例
如,下面语句将输出结构体变量today 的内容:
cout << today. da_year <<"年"<<today. da_mon << "月"<<today. da_day <<"日"<<endl;

注意

每次定义一个结构体变量,他都会开辟好内存了,只不过没有初始化,也就是说,结构体里面的变量已经开辟好对应的空间了,有了地址了,只是里面还没赋值。

结构体的初始化和使用举例



结构体与数组

结构体中的数组举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结构体的成员可以是数组,其使用方法和简单变量相同。

struct StudentType
{
int id;//学号
char name[ 20];//姓名
int score[ 5 ];//五门课程成绩
int GPA;//平均分
}

StudentType xjtuStudent ;

//如果要访问结构变量xjtuStudent的第二门课程成绩,可以用如下方法:
xjtuStudent.score[1]


结构体数组举例

如果某个班有30名学生,需要对这个班所有的学生的成绩情况进行处理,在定义完上面的结构体后,因为这些学生的数据类型都是相同的,所以可以用一个有30个元素的数组来处理学生的数据。即进行如下变量声明:
StudentType xjtuStudent[ 30];
这样,每一个数组元素都是一个结构体。

结构体与指针

指向结构体指针的使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct StudentType
{
char id[10];//学号
double score[5];//五门课程成绩double GPA;
double GPA;//平均分

}
StudentType xjtuStudent ;

//这里就定义了一个指向结构体的指针ptr,并将xjtuStudent的地址赋给它。
StudentType *ptr = &xjtuStudent;

//通过指针访问结构的成员要用箭头操作符->
ptr->score[1] = 90 ;


指向结构体的指针的图示

结构体的地址理解(我自己的理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

结构体的地址理解(我自己的理解)
有一个结构体
Struct T
{
A a;
B b;
C c;
}
里面有三个数据类型ABC,创建一个结构体对象,这个对象会在内存划一个连续的存储空间来存储abc三个数据,这个存储空间的的大小=存储数据类型A的大小+B的大小+C的大小,
假设创建一个结构体对象t,
a的空间地址为d1d2,地址长度为2。
b的空间地址为d3d4d5,地址长度为3。
c的空间地址为d6d7d8d9,地址长度为4。
t的地址就等于d1d2d3d4d5d6d7d8d9,地址长度为9。

enum枚举类型

枚举类型的基础知识

枚举类型的理解

如果某个数据项的取值范围仅限于少数几种可能的值(如星期几的取值只能是从星期一~星期日7种可能,比赛结果只能是输﹑赢、平3种),则可以将该数据项定义为枚举类型数据。
所谓枚举就是指在定义某个类型的时候,时就将其可能的取值都一一列举出来,这样,这个类型的变量的值就只限于列举出来的值的范围内,不能再取其他值。采用枚举类型可提高程序的可读性。

枚举定义举例

定义星期类型

定义了枚举数据类型后,就可以声明该类型的变量,在如下语句中
WeekdayType workday ;
变量workday只能取枚举类型中列出的符号值,可以是从SUNDAY ~ SATURDAY中的任何一个,不能再取没有列出的值。例如
workday = MONDAY;

枚举类型元素值说明

1.每个枚举元素实际上是一个以其所在位置顺序为值的常量。编译器会按照其定义时的顺序为它们取值为0、1、2、…、n -1。例如,对于枚举类型WeekdayType来说,MONDAY的值为1。
如果要打印变量workday 的值,可使用常规的输出方法,打印出的值只会是0、1、2、…、6这样的序号值。因此,枚举类型无法进行直接的输人和输出,要想获得变量的符号值,必须采用间接的方法。

2一般情况下,一个枚举类型中的各枚举值从0开始顺序取值。在前面的例子中,从SUNDAY ~SATURDAY分别取值0、1、…,6。但是,在定义枚举类型时,也可以对各枚举符号进行初始化,改变其对应的顺序值。例如:

从SUNDAY ~SATURDAY 所对应的值分别为7、1、2、…,6。
3.枚举变量可以直接输出,但不能直接输入。
4.不能直接将常量赋给枚举变量。

程序例子

主函数


输入
bl

输出
The color you ‘ve chosen is blue

强枚举类型的基础知识

强枚举类型的理解

C++11中的enum class是一种强类型枚举,与普通的枚举类型相比它可以避免枚举值之间的隐式转换。

强枚举类型定义举例

enum class EGameState :short { EWait; EPlaying; };
这段代码定义了一个枚举类型EGameState,它有两个成员:EWait和EPlaying。
其中,使用了short关键字来指定枚举类型的底层数据类型为short。枚举类型的底层类型为short,表示枚举类型中的元素的值都是short类型。

强枚举类型地特性

与旧版C++中的“非域内枚举”(unscoped enums)相比,enum class有以下优点

  1. 防止命名空间污染
  2. 不允许隐式转换为int
  3. 不允许不同枚举类型之间的比较

枚举类型和强枚举类型的区别

在C++中,枚举类型的元素是整数常量,它们之间可以进行隐式转换。例如,以下代码演示了如何使用旧版C++定义一个颜色枚举类型:

enum Color { red, green, blue };
这里,red、green和blue是枚举类型中的元素,它们分别被赋值为0、1和2。在这种情况下,枚举类型的元素可以隐式转换为int类型。

而在C++11中,enum class是一种强类型枚举,它可以避免枚举值之间的隐式转换。例如,以下代码演示了如何使用enum class定义一个颜色枚举类型:
enum class Color { red, green, blue };
这里,red、green和blue是枚举类型中的元素,它们不会被赋予任何整数值。在这种情况下,枚举类型的元素不能隐式转换为int类型。

强枚举类型不允许不同枚举类型之间的比较的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在C++中,强类型枚举是一种类型安全的枚举,它可以避免枚举值之间的隐式转换。例如,以下代码演示了如何使用enum class定义一个颜色枚举类型:

enum class Color { red, green, blue };
这里,red、green和blue是枚举类型中的元素,它们不会被赋予任何整数值。在这种情况下,枚举类型的元素不能隐式转换为int类型。

因此,如果你尝试将一个强类型枚举类型的元素与另一个强类型枚举类型的元素进行比较,编译器会发出错误。例如,以下代码演示了如何使用enum class定义一个颜色枚举类型,并尝试将其元素与int类型进行比较:

enum class Color { red, green, blue };
int main()
{
Color c = Color::red;
if (c == 0)
{
// ...
}
return 0;
}
在这种情况下,编译器会发出错误,因为不能将Color类型的元素与int类型进行比较。

强枚举类型防止命名空间污染的理解

C++11中的强类型枚举类(enum class)是为了解决C++98中的枚举类型(enum)在使用时可能会造成命名空间污染的问题。在C++98中,枚举类型是不限定作用域的,枚举中的成员可不加命名空间限定符随意使用,但是不限定作用域的会造成命名空间污染。
如果你在程序中使用了多个枚举类型,而这些枚举类型中有相同的成员名称,那么这些成员名称就会互相冲突,从而导致程序出现错误。这种情况就叫做“命名空间染”。

举个例子你有两个枚举类型,分别是“颜色”和“水果”,它们都有一个成员名称叫做“红色”。在C++98中的定义中
enum 颜色{红色,黄色,蓝色};
enum 水果{苹果,橙子,红色};
在这种情况下,使用枚举中的成员可不加命名空间限定符,那么就会出现相同成员名称互相冲突的问题,当你使用“红色”的这个元素时,编译器就不知道“红色”的这个值到底是哪个枚举类型的。

而当你使用C++11中的强类型枚举类去定义,
enum class 颜色{红色,黄色,蓝色};
enum class 水果{苹果,橙子,红色};
在这种情况下强类型枚举类是限定作用域的,即可视为一个 class,那么你使用元素的时候需要加命名空间限定符,标示元素是属于哪个枚举类型,比如我要用水果这个枚举类型的红色的元素值,就得这样使用 “水果::红色”,不会出现命名空间污染

union共用体类型

union共用体的基础知识

union共用体的理解

例如:在某一个学校信息管理系统中有以下数据:
1.姓名name。
2.年龄age。
3.职业 job:取值可有两种,即student 或teacher。
4.职位postion :
对学生( student)而言,采用的是年级 grand分级,取值为1,2,3,4等。
对教师( teacher)而言,采用的是职称title分级,取值为Professor , AssociateProfessor 、 Lector等。
可见,name ,age , job等三项对学生和教师职员是一样的,但postion 的取值会因为不同人员的身份不同而采用不同的序列。对于某一个人来说,他不可能同时具备两种身份,所以grand和title不用同时存储。这样,就可以在编程时用同一内存段来存放两种不同类型的成员,但每一瞬间只有一个成员起作用。这种特殊的数据类型就是共用体(或称联合体)

共用体的定义形式

1
2
3
4
5
union <共用体类型名>
{
<共用体类型的成员变量说明语句表>
}

共用体和结构体之间的区别

在结构体中,每一个数据成员都要单独占用一个存储空间,结构体类型的变量的长度等于其所有成员的长度之和。
共用体类型的变量的各个成员共享内存的同一段空间,其总长度等于其最大的成员的长度。

共用体的使用举例





作用域运算符::

作用域运算符基础知识

作用域

每一个变量都有其有效的作用域和生命周期,那么,一个变量只能够在它的作用域内使用。
范围简单分为:全局作用域,局部作用域,语句作用域。

作用域优先级

范围越小,优先级越高。

同名变量的问题

通常情况下,如果有两个同名变量,一个是全局变量,另一个是局部变量,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。

程序举例

程序的输出结果
a:20

结果的说明
在test函数的输出语句中,使用的变量a是test函数内定义的局部变量,因此输出的结果为局部变量a的值。

作用域运算符的作用

作用域运算符的作用就是跟编辑器说明变量所属的领域。
这可以用来解决局部变量与全局变量这类的重名问题(不只是局限于变量)。

作用域运算符的使用

全局变量的使用

使用方式
如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上作用域运算符,此时这个变量就是全局领域的变量。

举例的程序

程序结果
局部变量a:20
全局变量a:10

类的成员函数使用

使用的方式
如果声明了一个类A,类A里声明了一个成员函数void f(),但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成void A::f(),表示这个f()函数是类A领域的成员函数。

举例的程序

命名空间的使用

使用方式
如果你在命名空间A里定义了一个变量a(也可以是其他数据),那么要想使用命名空间A里的变量a,就得写成A::a,说明变量a是属于A空间领域。

命名空间 namespace

命名空间的基础知识

命名空间的理解

在c++中,符号常量、变量、函数、结构、枚举、类和对象等都是要有名称。工程越大,名称互相冲突性的可能性越大。
其次,C语言规定我们所命名的数据的名称不能与关键字冲突,但是并没有规定,这些名称不能与库中的数据的名称有冲突,而这在实际的操作的过程中就会产生错误
例如,在main函数中定义一个time,根据局部优先的原则,time变量就会被理解为一个局部变量。但当time作为一个全局变量出现的时候,由于time库中也含有一个名叫time的函数名,头文件被展开后,此time被理解为变量名还是函数名就会产生歧义。

为了避免,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入关键字namespace(命名空间),可以更好地控制标识符的作用域。

命名空间的语法

以一个程序举例

命名空间的嵌套使用

以一个程序举例

命名空间别名

以一个程序举例

注意

  1. 命名空间只能全局范围内定义(以下错误写法

  2. 命名空间是开放的,即可以随时把新的成员加入已有的命名空间中

  3. 声明和实现可分离

  4. 无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接

using语句的基础知识

using 的理解

每次要使用命名空间中的变量都需要写很多额外的内容,using语句可以简化操作。using声明指令可使得指定的标识符可用。using编译指令使整个命名空间标识符可用。

using声明使用举例

using声明指令可使得指定的标识符可用。

using声明重载的函数

如果命名空间包含一组用相同名字重载的函数,using声明这个函数名,就声明了这个重载函数的所有集合
举例

using编译指令的使用举例

using编译整个命名空间,使整个命名空间的标识符可用。

使A的整个命名空间的标识符可用,也被称作A命名空间的完全展开。

using指令的二义性问题

使用using声明或using编译指令会增加命名冲突的可能性。也就是说,如果有名称空间,并在代码中使用作用域解析运算符,则不会出现二义性。


using namespace std的理解

using namespace std的理解

using namespace std就是将std命名空间完全展开。我们可以直接的使用std命名空间里的数据。
而std是C++标准库(iostream)的命名空间,标准库的所有标识符都是在std的命名空间中定义的,或者说标准头文件中函数、类、对象和类模板是在命名空间std中定义的。因此,当引入标准库头文件之后,需要执行using namespace std,这样才能使用std命名空间中的标识符。

编译预处理

编译预处理的基础知识

编译预处理的理解

将程序编译的过程分为预处理和正式编译两个步骤是C++的一大特点。在编译C++程序时,编译器中的预处理模块首先根据预处理命令对源程序进行适当的加工,然后才进行正式编译。
所有的预处理命令均以符号“#”开头,且在一行中只能书写一条预处理命令(过长的预处理命令可以使用续行标志“\”续写在下一行上)。预处理命令不是C++语句,所以结束时不能使用语句结束符(分号“;”)。

预处理的分类

C++中有3种主要编译预处理命令:文件包含、宏定义和条件编译。

文件包含指令 #include

文件包含指令#include的基础知识

文件包含的理解

文件包含是编译预处理命令中的一种,它是指一个程序将另一个指定文件的内容包含进来,即将另一个程序文件在编译时嵌入到本文件中。

举例,某个源程序文件a.cpp,程序里面用了文件包含指令 #include <b.h>,作用就是把b.h这个头文件里的代码内容,搬到到a里面来,也就是说使用文件包含指令就可以把将各个文件拼接起来

命令格式

#include<文件名>#include"文件名"

如果使用了尖括号,则预处理程序在系统规定的目录(通常是在系统的include子目录)中查找该文件。

如果用双引号,则编译预处理程序会在双引号里面的路径去找到该文件,路径可以是相对路径,也可以是绝对路径。
比如 #include "F:\test.h", F:\test.h是绝对路径,该指令就会去F盘去查找test.h头文件,查找到则嵌入进来。
而如果是 #include"test.h",test.h等同于.\test.h,是相对路径,该指令就会在当前写了 #include"test.h"指令的源文件目录去找test.h头文件查找到则嵌入进来。
如果找不到则再去由操作系统的path命令所设置的各个目录中去查找。如果仍然没有查找到,最后再去上述规定的目录( include子目录)中查找。

绝对路径

从根盘符开始的路径,比如F:\test\test.h,表示F盘里的test文件夹,test文件夹里的test.h

相对路径

从自身所在的文件夹开始的路径,比如.\表示当前文件所在的文件夹里

路径表示

一般用反斜杠/或者双斜杠\\

文件包含指令的原理

[[文件包含原理 1.mp4]]

#include指令的运行原理

源程序文件执行include了头文件b.h,那么其作用效果就是等同于把include头文件b的这条指令,换成头文件b里的代码内容。也就是说include头文件b.h指令的位置已经没有了include头文件b.h这条指令,而是存放头文件b里的代码内容。

用include指令拼接代码的举例

假如有一个源程序文件(我们称为编译单位),include了一个头文件b.h,而b.h的头文件里面include了一个头文件a.h,最终编译单位在进行编译预处理时,就会把include头文件b的指令,换成头文件b里的代码内容。

而头文件b的代码内容,本身最开始的时候也include了一个头文件a.h,那么在头文件b的代码里,include头文件a.h的这个指令就会被换成头文件a里的代码内容,最终头文件里b的代码内容,就是由头文件a的代码内容和头文件b自身的内容组成,然后都一起被拼接到编译单位里。最终编译单位的代码内容,就是头文件a代码内容+头文件b自身代码内容+编译单位自身代码内容。

(文件的代码内容=头文件的代码内容+自身代码内容)
如下图

编译器头文件检测链接功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//编译器会有一个功能,用来检测链接的头文件。
//我写了一个头文件A.h,内容如下
struct A
{
void ptf()
{
printf("testOK");
}
};

//这时候头文件A.h里面因为没有头文件stdio.h,所以编译器会报printf报错
//这时候我写了一个test.cpp文件内容如下

#include <stdio.h>
#include "A.h"
int main()
{
A a;
a.ptf();
}
//在test.cpp里,我用文件包含指令,include了stdio.h和A.h,将这三个文件拼接在test里。这时候编译器就检测include了A.h的test,再include了A.h之前已经经先include了stdio.h,A.h里的printf函数就不会报错了。

重复include问题的解决

重复include的错误

假如编译单位先include头文件a,然后include头文件b,而头文件b本身也include头文件a,根据include的运行原理,我们就知道最终编译单位的代码内容=头文件a代码内容+头文件a代码内容+头文件b自身代码内容+编译单位自身代码内容。这样就是重复include头文件a,编译器就会报错,而用pragam once指令就可以解决该问题。

pragam once指令解决重复include问题

在头文件a的最开头,编写 #pragam once,该指令就会让这个头文件,在同一次拼接代码的过程中,只参与一次编译预处理。
还是用上面的例子编译单位先include头文件a,然后include头文件b,而头文件b本身也include头文件a。
在编译预处理阶段,编译单位先执行include头文件a指令,而头文件b本身虽然也有include头文件a的这条指令,但是头文件a已经了参与一次编译预处理了,那么在这次拼接过程中,头文件b就不会在执行include头文件a。最终编译单位的代码内容=头文件a代码内容+头文件b自身代码内容+编译单位自身代码内容,这样就不会重复include

条件编译解决问题

每次在编写xxx.h的头文件时,编程书上都会让我们在代码的前后加上如下的三句代码:
#ifndef XXX_H
#define XXX_H
……
#endif
其中……代表中间具体的功能语句。
这三句代码中,XXX_H是预处理指令中符号化常量名。编写这些语句时,预处理指令中符号化常量名使用的规则是将头文件名全部使用相应的大写字母,然后把.改成_。当然,这只是一种约定俗成的方法,如果使用其他的名字,只要不产生名字冲突,也都是合法的。
通过这种方法,也可以避免重复include的问题

条件编译解决问题的原理

当执行 #ifndef XXX_H语句时,编译器首先检查宏XXX_H是否已经定义过,如果已经定义过,那么就说明xxx.h头文件已经被引入,就可以把#ifndef XXX_H一直到 #endif之间的代码段跳过,继续编译下面的代码。

如果没有定义过,那么就会执行#define XXX_H语句来定义宏XXX_H,然后 #ifndef XXX_H一直到 #endif之间的代码段也就不会被跳过,而是会被编译器编译。

文件引入该头文件之后,如果再次引入该头文件时,编译器虽然会将这部分代码引入到文件里,但是编译器发现XXX_H已经被定义过,编译器就会跳过#ifndef XXX_H一直到 #endif之间的代码段。避免重复定义和编译。

交叉include问题

交叉include问题举例

头文件B

头文件A

test文件

此时程序编译就会报错

分离h和CPP文件的解决办法

[[文件包含原理.mp4#t=26:00,32:18]]
解决办法1∶分离基础类型,避免交叉依赖

解决办法2:分离h和cpp文件,包括使用指针对细节的使用体现在cpp文件中,cpp文件不被交叉包含。

头文件的编写

编写的步骤

1创建.h文件
2在文件的第一行写明, #program once,主要是为了防止头文件被重复引用,也可以用条件编译的方法来解决重复include问题
3引入在本文件要用的头文件
4在头文件里写上你的代码(一般声明写入头文件中)

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include <iostream>
#include "Point.h" // include写好的点类头文件

using namespace std;

class Circle // 定义一个圆的类
{
Point Cir_cen; // (点类)圆心
int Cir_R; // 半径
public:
void setCir(int x, int y, int r); // 初始化圆的属性

int Get_R(); // 得到圆的半径
double Distance(Point& p); // 判断一个点和圆的关系
};

多文件的编译执行

普通的多文件编译执行的方法(调用其他文件的函数的方法)

方法1,直接在头文件中声明和定义类和函数,然后在要调用的文件里加上这个头文件,既可以调用这个头文件类和函数。

方法2,首先 把函数或者类的声明写在头文件(A.h)里,然后源文件(A.cpp)里引入头文件A.h,并且写上函数的定义(源文件和头文件不必同名),然后然后在要调用的文件里加上这个头文件A,既可以调用头文件A的函数。

类模板的多文件编译问题

C++在编写类模板时,是不能让类模板声明写在一个头文件A.h里,然后实现写在一个源文件A.cpp,最后让main去include A.h。而这种行为是不允许的,最后编译链接就会出错。具体原因编译器在实例化一个类时,需要知道该类的所有确定的信息,如果是普通的类这是完全由头文件(.h)中类的声明决定的。但是对于模版类,此信息不确定,于是编译器只是存放一个符号,而把这一个步骤放到最后链接时来完成。而编译器在编译模版类的实现文件(.cpp)时没有发现其他地方有这个类的实例化。最终,到链接阶段找不到类模版的实例,出错

类模板的多文件编写方法

方法1
就是让类模板的声明和实现放在同一个文件中,并命名为.hpp文件,这样只需要在main函数所在文件头文件加上”***.hpp“就可以了
(注意hpp是约定的名称,并不是强制,命名为.h文件,也是一样的效果。)

方法2
首先 把函数或者类的声明写在头文件(A.h)里,然后实现文件A.cpp里引入头文件A.h并对其实现。最后最后让main去include A.cpp

第二种方法举例




宏定义 #define

宏定义基础知识

宏定义的理解

宏定义是预处理指令的一种,就是用#define指令去定义宏 ,

#define MAX_NUMBER 1000,真正含义是要求在正式编译程序前,对源程序进行预处理时,将源程序中,所有的符号名MAX_NUMBER(但不包括出现在注释或字符串中的MAX_NUMBER),分别替换为字符序列“1000”。因此,在正式编译时,符号名MAX _NUMBER已经不存在。
若程序中再出现这样的语句MAX_NUMBER = 70;则是错误的,因为会变为“70 = 1000 ;”,显然这是一个错误的赋值表达式语句

实际上,宏定义更适合于C语言编程而不是C++编程。学习宏定义的目的是为了读懂C语言的遗留代码。

无参数宏定义

无参数宏定义格式

#define  <宏名>  <替换序列>
其中“宏名”是一个标识符。为了和变量有所区别,习惯上在为无参数宏起名时只使用大写字母。

举例

#define PI 3.14159
该宏定义命令为常量3.14159起了一个符号化的名字PI。这样,在该宏定义命令之后的程序中,均可以使用符号常数PI表示数值3.14159。
例如:s = Pl*r * r ;使用符号常量的程序可读性好,易于阅读理解。

取消宏定义指令 #undef

取消宏定义命令#undef 用于撤销对一个符号名的定义。

取消宏定义指令 #undef的使用举例

#define DEBUG 1
//在本程序段落中定义了一个值为1的符号常数DEBUG
#undef DEBUG

#undef DEBUG指令之后,DEBUG这个标识符就不会被替换成常数1
如果没有#undef 命令 ,则宏定义起作用的范围为自 #define 命令起至该源程序文件末尾 。

宏定义和const的区别

宏定义是在预处理阶段执行的。
宏定义的作用只是对代码中出现的宏名替换成我们定义好的文本或数值,不进行任何类型检查和编译器检查。

const是在编译阶段使用。
const的作用是将某个数据类型的变量声明为常量。
使用 const 修饰的变量具有类型,可以进行类型检查和编译器检查。
所谓的类型检查,就是如果使用错误的类型来操作常量,编译器将会发出错误提示。比如const int a=”abc”
所谓的编译检查,就是const修饰的变量,编译器会检查在定义时否被赋值,以及赋值后是否被修改。使用const修饰的变量在定义时没有赋值,或者在赋值后被试图修改,编译器就会报错

带参数的宏定义

带参数宏定义格式

#define <宏名> (<参数表>) <带有参数的替换序列>

举例

#define max(a,b) ((a)>(b)?(a):(b))
带参数的宏的用法很像函数,此时在程序里写上x=max(x,10);
这个带参数的宏的确切含义为:通知编译程序中的预处理模块在对应用程序进行处理的过程中,一旦遇到形如max()的串时就要进行转换,在转换时还要对参数进行替换处理。上述宏定义经过转换后成为:
x=((x)>(10)?(x):(10));
在利用宏命令定义带参数的宏时,要注意宏名与括号之间不能有空格,所有的参数都要用逗号分开,并且均应出现于右边的替换序列中。

带参数的宏定义和函数的区别

虽然带参数的宏很像函数,但这两者之间的区别是很明显的。编译程序在处理带参数的宏时,并不是用实参的值进行代入计算,而只是简单地将替换序列中替换原本宏,在替换过程中,还要对参数进行替换处理。

带参数的宏定义容易出现的错误

在书写带参数的宏时,要防止由于使用表达式参数带来的错误。
例如,定义了一个用于计算圆面积的宏:
#define circle_area(r)  r * r * 3.14159
在计算
s = circle_area( x +16)
时就会出问题。将参数x +16代入上述宏定义中,即
s = x + 16* x + 16* 3.14159
#define circle_area( r) (( r) * ( r) * 3.14159)
就没有问题。因此,在定义带参数的宏时应将每个参数和整个替换序列都用括号括起来,以防止可能出现的计算错误。

条件编译

条件编译的理解

在调试程序期间,经常希望输出一些调试用的信息,而在调试完成后,就不再需要这些输出信息。要解决这个问题,一种方法是逐一从源程序中删去输出这些调试信息的程序段落,或者将这些程序段落用注释标记括起来。显然,这样做要花费相当大的工作量,而且今后再要进行调试时,恢复这些调试程序段也非常麻烦。另一种方法就是使用编译预处理中的条件编译命令。

#if条件编译语句的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#if <条件1 >
<程序段落1>
#elif <条件2>
<程序段落2>
.
.
.
#elif <条件n >
<程序段落n >
#else
<默认程序段落>

#endif
//条件编译中所使用的条件只能是由常数构成的表达式。如果该常数表达式的值不为0,表示条件成立,否则表示条件不成立。一般情况下,在#if中都是使用由#define 指令产生的符号常数进行测试。
#if指令和#elif指令是用于对一个或多个预处理符号进行比较的,如果符号的值为非零(true),则执行程序段落;否则,忽略该程序段落,继续判断下一个程序段落。#else指令表示程序段落的默认值,即如果前面的所有条件都不满足,则执行这个程序段落。#endif指令表示条件编译的结束。
在预处理阶段,编译器会根据条件表达式,的结果来判断是否编译 #if里的程序段落,如果结果为真,则编译该代码段,否则忽略该代码段,不会对其进行编译。

条件编译的举例

编译预处理命令的一个常见的用途是将调试代码插入应用程序中。例如,程序员可以足义一个叫做DEBUG 的符号常数,其值可以定义为1或者0。在程序中的任何地方,都可用以下方式插入调试信息:
#if DEBUG==1
<此处为调试代码>
#endif
在开发程序时,先将DEBUG定义为1,则插入的调试代码被编译进目标程序,可以输出一些帮助跟踪错误的信息。
一旦程序工作正常,就可以将DEBUG重新定义为0后,再次编译程序,即可从目标程序中去掉调试代码。
在预处理阶段,编译器会根据条件表达式 DEBUG==1 的结果来判断是否编译 #if#endif 之间的代码,如果结果为真,则编译该代码段,否则忽略该代码段,不会对其进行编译。

if语句与条件编译的区别

可以看到,条件编译的用法和C++的if语句的用法非常相似。那么能否用if语句去代替#if条件编译,去实现调试代码的选择输出呢?
int DEBUG = 1 ;
if(DEBUG)
<调试代码>
在调试时,全局变量DEBUG被赋值1,这时“调试代码”起作用,输出调试信息。
待调试结束后,将赋给DEBUG的值改为0,则不再输出调试信息。

初看起来,这种用法与使用条件编译差不多,但实际上这两种结构有着本质的区别:if 语句控制着某些语句是否执行,而 #if 指令控制着某个程序段落是否被编译。
因此,上述使用C++条件语句的结构在调试成功后仍然被编译成目标代码,只是不再执行,成了一段废码;
而使用条件编译的调试结构在正式编译时根本不会被编译,目标程序中也没有废码,所以目标码占用的存储空间较少。

#ifdef#ifndef 条件语句的理解

此外,C++中还提供了另外两个和 #if 类似的条件编译命令 #ifdef#ifndef
例如:
#ifdef DEBUG
xxxx
#endif
只要前面定义了DEBUG(无论将DEBUG定义为什么值,包括0 ,甚至什么具体值也不是),则上述条件就成立。

#ifndef DEBUG
xxxx
#endif
在前面没有定义DEBUG时成立。