C++ 笔记
A passage about C++ - Introduction to Programming with C++_Notes
序
- C++融合了3种不同的编程方式:C语言代表的过程性语言、C++在C语言基础上添加的类代表的面向对象语言以及C++模板支持的泛型编程。
- 高级(high-level)语言致力于解决问题,而不针对特定的硬件。(可移植性)
- 一般来说,计算机语言要处理两个概念——数据和算法。数据是程序使用和处理的信息,而算法是程序使用的方法。
- C语言是过程性(procedural)语言,强调的是编程的算法方面。
- 虽然结构化编程的理念提高了程序的清晰度、可靠性,并使之便于维护,但它在编写大型程序时,仍面临着挑战。
- 为应付这种挑战,OOP提供了一种新方法。与C语言不同的是,OOP强调的是数据。
- OOP不像过程性编程那样,试图使问题满足语言的过程性方法,而是试图让语言来满足问题的要求。其理念是设计与问题的本质特性相对应的数据格式。
- 在C++中,类是一种规范,它描述了这种新型数据格式,对象是根据这种规范构造的特定数据结构。
- 泛型编程(generic programming)与OOP的目标相同——使重用代码和抽象通用概念的技术更简单。
基本知识
成员函数
.getline
读入一行,.get
读入一个字符。基本数据类型
- 整型:
int
整型,unsigned int
或unsigned
无符号整型,short
或short int
短整型,unsigned short
或unsigned short int
无符号短整型,long
或long int
长整型,unsigned long
或unsigned long int
无符号长整型。 - 浮点型:
float
单精度浮点型,double
双精度浮点型,long double
长精度浮点型。 - 字符型:
char
字符型,unsigned char
无符号字符型。 - 布尔型:
bool
。
一个有符号短整型数所能表示的数值中,有一半是负数,另一半是正数。而一个无符号短整型数所能表示的数值都是非负的。由于两种类型占用一样大的内存空间,因此存储在一个无符号整型数中的最大值,是一个有符号整型数所能保存的最大正数值的两倍。如果确定一个变量的值始终为非负,那么就将它声明为无符号数。
位/bit/二进制位
计算机的最小存储单元是字节(byte),1 byte = 8 bits。1 KB = $10^3$ bytes,1 MB = $10^6$ bytes,1 GB = $10^9$ bytes,1 TB = $10^{12}$ bytes。内存
- 内存由一系列有序的字节构成,用来存储程序和程序所操作的数据。可以把内存看作计算机执行程序的工作区域。
- 每个字节在内存中都有一个唯一的地址,地址用来在存储和查找数据时定位字节的位置。由于内存中的字节可以按照任意顺序进行读取,所以内存又称为随机存储器(Random Access Memory,RAM)。
- 内存是易失的,即断电后信息会丢失。
#
磅符号,表示一个预处理指令。每个子组成部分或语句要比嵌套它的结构多空两格。二元操作符的两端都应该增加一个空格。程序段之间要加空行。
标识符
标识符是程序中定义类似变量、函数之类元素的名字。
- 一个标识符是一个字符序列,可以包含字母、数字和下划线。
- 一个标识符必须以一个字母或一个下划线开头,不能以数字开头。
- 不能使用保留字作为标识符。
- 一个标识符理论上可以任意长,但我们使用的具体的C++编译器可能会有限制,使用31个字符或更短的标识符可保证程序的可移植性。
const
声明常量,常量名字大写。默认情况下,一个整数文字常量表示一个十进制整数,一个八进制整数文字常量,使用前缀0,十六进制整数文字常量,使用前缀
0x
或0X
。取模运算
%
的运算对象只能是整数。自增
++
和自减--
位于前后的区别。
通常,IDE允许在辅助窗口中运行程序。程序执行完毕后,有些IDE将关闭该窗口,而有些IDE不关闭。为查看输出,必须在程序的最后加上一些代码:
1
2
3 cin.get(); // add this statement
cin.get(); // and maybe this, too
return 0;
cin.get()
语句读取下一次键击,因此上述语句让程序等待,直到按下了Enter
键(在按下Enter
键之前,键击将不被发送给程序,因此按其他键都不管用)。如果程序在其常规输入后留下一个没有被处理的键击,则第二条语句是必需的。
分支(branch)
数值类型转换
static_cast<type>(value)
或C类型转换(type)value
/type(value)
。浮点数有限制的精度,涉及浮点数的计算会导致舍入误差,因此两个浮点数之间的相等性测试是不可靠的。
abs()
在cmath
库,rand()
在cstdlib
库,srand(time(0))
种子。&&
有条件的与运算符/短路与运算符,||
有条件的或运算符/短路或运算符,&
与位操作,|
或位操作。switch
语句(书签)
一个switch
语句基于一个变量的值或者是一个表达式来执行语句。
1 | switch (switch-expression) |
switch
表达式必须产生一个整型值,而且必须放在括号内。value1
,…,valueN
是整型常量表达式,即表达式中不能包含变量,如1 + x
就是非法的。这些值必须是整型值,不能是浮点型值。- 当某个
case
语句中的值与switch
表达式的值相等,则从此case
语句开始执行后续语句,直至遇到一个break
语句或者到达switch
语句末尾。 default
情况是可选的,它用于指出,在任何指定情况均与switch
表达式不匹配时,执行什么动作。- 关键字
break
是可选的,break
语句会立刻终止switch
语句。
在需要使用
break
的地方不要遗漏。当某个case
语句被匹配时,会从这个case
语句开始执行,直至遇到一个break
语句或到达switch
语句的末尾。这种现象被称为直通行为(fall-through behavior)。
条件表达式
boolean-expression ? expression1 : expression2;
(C++中唯一的三元运算符)。运算符优先级(从高到低)
- var++ var - -
- +(一元) -(一元) ++var - -var
- static_cast(v) (type)(Casting)
- !
- * / %
- +(二元) -(二元)
- < <= > >=
- == !=
- &&
- ||
- = += -= *= /= %=
- 逻辑错误叫做bug,找到和改正错误的过程叫做调试(debugging)。
字符与字符串(character&string)
在
cmath
头文件中的函数:sin(radians)
正弦,cos(radians)
余弦,tan(radians)
正切,asin(a)
反正弦,acos(a)
反余弦,atan(a)
反正切,exp(x)
返回$e^x$的值,log(x)
返回自然对数的值,log10(x)
返回常用对数的值,pow(a,b)
返回$a^b$的值,sqrt(x)
返回$\sqrt x$的值(x $\geqslant$ 0),ceil(x)
向上取整(double
型),floor(x)
向下取整(double
型)。在GNU C++中,
min(a,b)
、max(a,b)
和abs(x)
函数都定义在cstdlib
头文件下,而在Visual C++ 2013中min(a,b)
和max(a,b)
定义在algorithm
头文件下。一个字符数据类型
char
代表一个单独的字符,一个字符在计算机中存储为许多0和1,把一个字符映射到它的二进制码叫做编码(encoding)。编码方式有许多,如ASCII
,一种8位的编码方案来表示所有大写字母、小写字母、数字、标点符号和控制字符。在大多数系统上,char
类型是1字节。转义序列
转义序列 | 名称 | ASCII值 |
---|---|---|
\b |
回退符 | 8 |
\t |
制表符 | 9 |
\n |
换行符 | 10 |
\f |
换页符 | 12 |
\r |
回车符 | 13 |
\" |
双引号 | 34 |
字符
' '
、'\t'
、'\f'
、'\r'
和'\n'
被称为空白字符(whitespace character)。
一个字符能被转换为任何数值类型,反之亦然。当一个整数被转换为一个字符的时候,只有低8位能被使用,剩下的部分就被忽略掉了。
当一个浮点数转换为一个字符类型时,浮点数先转换为
int
类型,然后再转换成char
类型。当一个char
类型转换成一个数值类型时,字符的ASCII
码被转换到指定的数值变量中。char
类型被看作byte长度的整数。所有的数值运算符都可以用于char
操作。当其中的另一个操作对象是数字或者是字符时,char
会自动转换为数字。cctype
头文件存储了测试字符和转换字符的函数,返回int
值。
函数 | 描述 |
---|---|
isdigit(ch) |
如果指定的字符是数字,则返回true |
isalpha(ch) |
如果指定的字符是字母,则返回true |
isalnum(ch) |
如果指定的字符是字母或数字,则返回true |
islower(ch) |
如果指定的字符是大写字母,则返回true |
isspace(ch) |
如果指定的字符是空白字符,则返回true |
tolower(ch) |
返回指定字符的小写形式 |
toupper(ch) |
返回指定字符的大写形式 |
string
不是原有的数据类型,它被认为是一个对象类型(object type)。当声明一个对象类型的变量时,变量实际上代表了一个对象。声明一个对象实际上是创建一个对象。对象是通过类定义的,string
就是一个预先定义在string
头文件中的类。
为了使用
string
类型,需要#include <string>
。
string
对象的简单函数
函数 | 描述 |
---|---|
length() |
返回字符串中的字符个数 |
size() |
同length() |
at(index) |
返回字符串中指定位置的字符 |
string
类的函数只能被特定的string
实例调用,故这些函数被称为实例函数(instance function)。调用一个实例函数的语法是objectName.functionName(arguments)
,注意长度与位置不同。
默认
string
被初始化为空字符串,即string s;
与string s = "";
等价。stringName[index] = char
:重写index + 1
处字符。直接连接两个字符串是非法的。例如,下面的代码是不合法的:
1 | string cities = "London" + "Paris"; |
然而,下面的代码是正确的,因为它先把字符串s和”London”连接起来,然后新的字符串再把”Paris”连接起来。
1 | string s = "New York"; |
可用==、!=、<、>、<=、>=从左到右比较字符串。
cin
可读取字符串,以空白字符结束。string
头文件中的getline
函数可读取字符串:
1 | getline(cin, s, delimitCharacter); |
第三个参数
delimitCharacter
有一个默认值'\n'
,读到但不存储在string
里。
- 格式化控制台输出
iomanip
头文件,常用的流操作有
操作 | 描述 |
---|---|
setprecision(n) |
设定一个浮点数的精度 |
fixed |
显示指定小数位数的浮点数 |
showpoint |
即使没有小数部分也显示以零补足的小数点后位数 |
setw(width) |
指定打印字段的宽度 |
left |
调整输出到左边 |
right |
调整输出到右边 |
- n是小数点前后位数的总和(取近似值),
setprecision
操作的作用是直到精度改变之前,一直保持效果(不够一个整数,则被忽略)。 fixed
操作来强制数字显示为非科学记数法的形式,显示小数点后的位数默认情况下能修复小数点后6位(与setprecision
搭配使用)。setw
只影响下一次输出,默认右对齐(可以在前面用cout << left <<
或cout << right <<
调整)。宽度可自动增加。- n和width可以是整数变量、表达式或常量。
- 简单的文件输入输出(需
#include <fstream>
)
- 写入文件
ofstream output;
(声明此类型变量),output.open("numbers.txt");
(创建文件,若存在则销毁重建)。 - 创建输出对象并打开文件
ofstream output("numbers.txt");
。 - 写入数据
output <<
,输入完成output.close();
。 - 读取文件
ifstream input;
,input.open("numbers.txt");
,若不存在将出现unexpected error
。 - 创建输入对象并打开文件
ifstream input("numbers.txt");
。 - 读取数据
input >>
,完成input.close();
。
- C字符串是一个字符数组,以
'\0'
(空终结符)结尾,char city[7] = "Dallas";
第一条语句是一个C字符串,第二条语句是一个字符数组。第一个有7个字符(包括最后的空终结符),第二个有6个字符。
1 | char city1[] = "Dallas"; // C-string |
输出C字符串
cout << s;
。读取有空格的C字符串,iostream
头文件中的cin.getline
函数:cin.getline(char array[], int size, char delimitedChar)
,其中delimitedChar
默认为'\n'
。当遇到'\0'
或者读取了size-1
个字符之后,函数停止读入字符,最后一个字符被空终结符替代。C字符串传递给函数时可以不用传递它的长度。
size_t是一个C++类型,对于大多数编译器,它和
unsigned int
相同。复制C字符串必须使用
strcpy
函数。
循环(loop)
不要在循环继续条件中使用浮点数的相等性判定。
输入和输出重定向
如果有许多数据需要输入,那么可以在一个文件中存储数据,使用空格分隔开,如input.txt
,然后用SentinelValue.exe < input.txt
命令运行程序,这个命令叫做输入重定向(input redirection)。SentinelValue.exe
可以通过编译器命令行获取:
1 | g++ SentinelValue.cpp -o SentinelValue.exe |
input
对象的eof()
函数来检测是否已经到了文件末尾(返回true
或者false
)。循环:
do-while
、while
、for
。嵌套循环运行可能需要很长时间。continue
跳出一次迭代(只能在循环内使用),break
结束整个循环。例:十进制转换为十六进制
1 | #include <iostreram> |
函数(function)
主函数由操作系统调用,用于启动程序的执行。其他函数必须由函数调用语句来执行。
每当一个函数被调用时,系统都会创建一个活动记录(也成为活动结构)来存储其参数和变量并将活动记录放置到一个叫做调用栈的内存区域。调用栈也被称为执行栈、运行栈或者机器栈,也经常简称为栈。
void
函数是不需要return
语句的,但可以在void
函数中使用return
语句结束函数,返回调用者。这种用法并不常见,但有时可有效绕过一个void
函数的正常控制流。有时候需要在不正常的情况下终止程序,这个功能可以通过调用
exit(int)
函数,在cstdlib
头文件中。可以通过传递任何一个整数来调用这个函数的显示程序中的错误。默认为值传递,参数顺序关联。函数可以被用来减少冗余的代码,并使代码可以复用。函数可以用来模块化代码并提升程序的质量。
函数的重载:用同样名字命名函数,签名不同,不同的参数列表。重载函数必须有不同的参数列表,你不能依据不同的返回值类型重载函数。数学函数都在
cmath
头文件中重载。在函数原形中只需列出参数类型,声明函数(不用实现)、定义函数(给出执行函数的函数体)。
声明函数时可指定参数的缺省值,带缺省值的参数应该放在函数列表末尾,调用函数时,如果一个参数未给出,那么在它之后的参数也不能给出。
内联函数(inline function)可提升短函数性能,不会被调用,避免函数调用的开销,实际上编译器将其代码复制到了每个调用点上。(在声明函数前加上关键字
inline
)C++编译器会扩展每个对内联函数的调用,将函数代码复制过来替换调用。C++允许编译器对过长的函数忽略inline
关键字。函数内部定义的变量称为局部变量,定义在所有函数之外的变量是全局变量,可被其作用域内所有函数访问。局部变量没有缺省值,而全局变量的缺省值为0(default to 0)。
作用域
局部变量:从声明到包含它的程序块结束,
全局变量:从声明到程序末尾。声明在主函数之后的全局变量无法被主函数访问。
如果一个函数中定义了一个与全局变量同名的局部变量,那么在函数内部只有局部变量是可见的。应尽量避免使用全局变量。
改变生存周期:静态局部变量,在程序的整个生命周期中会一直驻留在内存中。
static
关键字。当一个函数结束执行后,其所有局部变量都会被销毁(自动变量)。作用:保留局部变量的值,以便在下次调用时使用。参数可以通过引用方式调用,使形参是实参的一个别名。因此,函数中参数的改变也改变了参数的实际值。引用变量,在变量名前或数据类型后加一个
&
。二者共享内存,共享相同的变量,形参和实参的类型要相同,否则值传递。引用传递的实参必须是一个变量,值传递时传入的参数可以是一个数值、一个变量或者是一个表达式,甚至是另一个函数的返回值。
常量引用参数
const int& num
。对于
string
类型的对象,引用传递比值传递更有效(节省内存),因为对象可能占据大量内存,然而对于int
和double
类型,区别是微不足道的。所以,如果原始的数据类型不需要在函数中改变,就可以简单地声明值传递。函数抽象(function abstraction)就是将函数的使用和实现分离。实现细节被封装在函数内,对调用函数的用户是隐藏的,这就是所谓的信息隐藏(information hiding)或封装(encapsulation),函数实现的信息对用户来说是隐藏在“黑箱”内的。
函数抽象的思想可以用于程序开发过程。当编写一个大程序时,可使用分治(divide and conquer)的策略,也称为逐步求精(stepwise refinement),即将原问题分解为若干子问题,子问题还可进一步分解为更小的,更易处理的问题。
使用函数抽象思想将设计和细节分离,最后再实现细节。实现上可以采用“自顶向下(top-down)”方法,就是按结构图,从上至下依次实现每个函数。对于还未实现的函数,可用桩(stub)函数代替。还可以用“自底向上(bottom-up)”方法,从下至上地实现结构图中的每个函数。每实现一个函数,编写一个测试函数,也称驱动(driver),测试其正确性。
逐步求精的好处:易编写、复用(减少代码冗余)、开发、调试、测试、修改、维护,更好地促进团队合作。
数组(array)
数组(array)是一种数据结构,是用来存储同类型变量的数据集合。声明数组时
elementType arrayName[SIZE]
,编译器为数组分配了SIZE
个elementType
型元素的空间。当一个数组被声明后,其元素初值是任意的。数组大小必须是常量表达式。数组下标是0基址的,范围是0~`arraySize-1。注意声明与访问不同,声明
int array[10],访问从
array[0]到
array[9]`。数组初始化语句:
double myList[4] = { 1, 2, 3, 4 };
其中myList[4]
也可以写为myList[]
。(不能分成两句写)C++允许只初始化数组的一部分元素(
cin >> myList[i];
),此时,其余元素被赋予0。注意,如果一个数组被创建,但还未初始化,那么其元素的值都是“垃圾”(不确定是什么内容,这一点与其他局部变量是类似的。只能用
for
循环输出数组,同样,只能用for
循环复制数组。string months[] = { "January", ..., "December" };
字符串数组。可将数组传递给函数,即数组的起始地址被传递给了函数中的数组参数(形参)(值传递)。语义上讲,这就属于传递共享,也就是函数中的数组和传递给函数的数组是同一个(改变同时发生)。
通常,向一个函数传递一个数组时,应该通过另一个参数将其大小也传递给函数,这样函数就能知道数组中包含多少个元素。否则,就需要将数组大小硬编码到函数中,或在全局变量中声明它。但哪种方法都不是一种灵活的、健壮的方法。
在函数中定义
const
数组参数,防止被修改。const
数组被传递给另外一个函数后,对应参数也必须声明为const
类型,确保一致性。数组不可作为函数值返回。二分搜索前提有序,无法找到返回
-low-1
。选择排序找到最小放在最前。
二维数组
matrix[2][1]
指第三行第二列。二维数组声明:
1 | int m[3][3] = { { 1, 2, 3 }, |
- 二维数组可作为函数参数,要指明列的大小。
面向对象编程(Object-Oriented Programming,OOP)
一个对象具有唯一的身份、状态和行为。状态/属性(state)用数据域(datafield)及它们的当前值表示,行为/动作(behavior)由一组函数定义。对一个对象调用一个函数就是请求对象执行一个操作。
相同类型的对象用一个通用的类来定义。一个类(class)是指一个模板、蓝图或约定(contract),定义了对象具有什么样的数据域和函数。
C++类中,用变量定义数据域,用函数定义行为。类就是用于创建对象的模板。
构造函数,可以执行任何动作,目的用来执行初始化动作和初始化对象的数据域。
类和对象可以用UML表示法来描述,UML是统一建模语言。UML类图中,+表示公有域,-表示私有域。
定义类末尾有分号,
public
表示所有的数据域,构造函数和普通成员函数都可以通过类对象来访问。缺省(default
)为私有的(private
)。Circle circle1(1.0)
创建对象,Circle.radius = 1
重新赋值,circle.radius
访问数据域,circle.getArea()
调用函数。构造函数是一种特殊的函数,与其他函数相比有下面3个不同点:
- 构造函数的名字必须与类名相同。
- 构造函数没有返回类型——即便返回
void
也不可以。 - 在创建对象时,构造函数被调用,它的作用就是初始化对象。
构造函数可以被重载,即可以有多个同名的构造函数,但签名不同,目的是用不同初始数据创建对象。
只能用构造函数来初始化数据域。作为一个类成员,数据域不能在声明时进行初始化。(无参构造函数
Circle()
)一个类的声明中可以不包含构造函数的声明,这种情况下,相当于在类中隐含声明了一个无参的空构造函数(缺省构造函数)。只有当程序员没有在类中显式地声明构造函数时,编译器才会自动提供缺省构造函数。
Circle::Circle():radius(1){}
与Circle::Circle(){ radius = 1; }
等价。对象的数据域没有无参构造函数时,有必要采用初始化列表的方法来初始化。
调用函数即执行操作。
objectName.datafield
引用对象中的一个数据域,objectName.function(arguments)
调用对象上的一个函数。某个数据域被称为实例成员变量或实例变量,某个成员函数被称为实例成员函数或实例函数,因为它们依赖于特定的实例。
类首字母大写,C++库中类小写。对象的命名参照变量。
一个类就是一个数据类型,
=
实现对象间内容的复制。sizeof
查看大小(字节数)。ClassName()
使用无参构造函数创建一个匿名对象,ClassName(arguments)
使用带参数的构造函数创建一个匿名对象。类定义和实现分离,
.h
文件(头文件末尾有分号)和.cpp
文件。类定义:列出数据域、构造函数原型和函数原型;类实现:构造函数和成员函数的实现。.cpp
文件要#include "Circle.h"
,Circle::...
二元作用域解析运算符,指明了类成员的作用范围。在集成开发环境中开发程序,如果主程序还使用其他程序,那么所有源程序文件都应包含在项目中。否则,就会导致连接错误。
包含保护(inclusion guard):避免头文件被多次包含(以
Circle.h
为例)
1 | #ifndef CIRCLE_H |
函数在类定义内实现,自动成为一个内联函数。
数据域私有
private
可以保护数据,并且使类易于维护。数据域封装,在类之外的程序中,无法通过直接引用类对象来访问它。
set
函数间接操作,get
函数返回其值。数据域声明顺序任意,最好先公后私。get
函数就是一个“访问器”(accessor),而set
函数就是一个“更改器”(mutator)。一个get
函数具有如下的函数签名:
1 | returnType getPropertyName() |
假如返回类型是bool
类型,则get
函数习惯上定义为如下形式:
1 | bool isPropertyName() |
- 一个
set
函数的函数签名如下:
1 | void setPropertyName(dataType propertyValue) |
在类中,只能为一个数据域声明一个成员变量,但一个变量名可用在多个不同的函数中声明多个局部变量。
类抽象(实现与使用分离)、封装(实现的细节封装起来,对用户隐藏)。
C字符串是以
'\0'
结尾的字符数组,string
类:string s = "Welcome to C++";
(这样更好string s("Welcome to C++");
)。也可以使用string
类的构造函数从C字符串来创建一个字符串,如下所示:
1 | char s1[] = "Good morning"; |
这里,s1是一个C字符串,而s是一个字符串对象。
- 把数字转换为字符串,使用
sstream
头文件中的stringstream
类:
1 | stringstream ss; |
如果希望一个类的所有实例共享数据,应该使用静态变量(static variable),也称为类变量(class variable)。静态变量机制在一个共同的内存位置中保存多个对象的变量的值。由于使用共同的内存位置,因此如果一个对象改变了静态变量的值,那么实际上同一个类的所有对象的此变量的值都被改变了。声明前
static
。只读成员函数,在函数头的结尾加上
const
,函数不会改变对象的数据域。和常量参数一样,只读函数也是一种防御式编程(defensive programming)。若函数不改变传递给它的对象内容,应该给该参数加上const
关键字。
只有实例函数可被定义为只读函数,静态函数是不能被定义为只读函数的。
面向过程范式要点是设计函数,而面向对象范式把数据和函数结合在一起形成对象(对象、对象的操作)。
类设计准则:内聚(cohesion)、一致(consistency)、封装(encapsulation)、清晰(clarity)、完整(completeness)、实例与静态(instance&static)。
指针(pointer)
指针变量也称指针,它是C++的一个强有力的特性,是C++语言的核心和灵魂,可以用来引用数组、对象或任何变量的地址。
指针变量保存的是内存地址,利用解引用运算符
*
可以访问指针指向的特定内存位置中的数据。&
为地址运算符,取变量的地址。声明指针
dataType* pVarName
,向指针赋予地址pVarName = &varName
。(推荐在单独的语句行中声明一个指针)对指针赋值,必须使用相同类型变量的地址。可以把指针变量赋值为同类型的指针,但是不能把一个指针变量赋予一个非指针变量。
应该总是保证对指针进行初始化,以避免错误。可以将一个指针赋值为0,表示指针未指向任何变量。
iostream
等C++库中定义常量NULL
为0,使用它来替代0能让程序可读性更高。C++允许使用关键字
typedef
来自定义同义类型typedef existingType newType
。指针为常量即指针指向一个不变的内存位置,但该内存位置处的实际值是可以改变的。
常量指针与指针常量的区别:
1
2 const int* p = &var;//常量指针
int* const p = &var;//指针常量
在C++中,数组名实际上是指向数组中第一个元素的指针(指向的位置不能改变)。数组与指针的联系是很紧密的,一个数组实质上是一个指针。而指向一个数组的指针可以像数组一样使用,甚至可以对指针使用下标变量。即
array[0]
与*array
等价,array[1]
与*(array + 1)
、p[1]
、*(p + 1)
等价。(int* p = array
)C++允许对指针加、减一个整数,效果是指针包含的地址值被增加或减少,变化的量是该整数乘以指针指向的元素的大小。可以用关系运算符对指针进行比较运算,以确定指针的先后次序。
由于C字符串可通过指针来访问,所以C字符串也被称为基于指针的字符串(pointer-based string)。
在C++中,函数的参数可以是指针,即可以在函数调用时传递指针参数(值传递)。例:
1 | void f(int* p1, int* &p2) |
这和下面的语句是等价的:
1 | typedef int* intPointer; |
- 如果使用指针
q1
和q2
调用函数f(q1, q2)
:
q1
是通过传值方式传给p1
的,所以*p1
和*q1
指向相同的内容。如果函数f
修改了*p1
(例如,*p1 = 20
),则*q1
也相应修改了。但如果函数f
修改了p1
(例如,p1 = somePointerVariable
),则q1
并未改变。q2
是通过传引用方式传给p2
的,所以q2
是p2
的别名,它们是等同的。无论函数f
修改了*p2
还是p2
,对应的*q2
和q2
也相应修改了。
函数中的数组参数都可以用指针参数来替换,即将
list[]
替换为*list
。有的参数的值(在函数执行过程中)不改变,为避免无意中修改它,应该把它声明为const
类型。在C++中,函数可以返回一个指针。
C++提供了操作数组的一些有用函数,在头文件
algorithm
中。函数min_element
和max_element
返回指向数组中最小和最大元素的指针,sort
函数可以对数组进行排序,random_shuffle
函数可以对数组进行随机洗牌,find
函数可以在数组中查找某个元素。所有这些函数的参数和返回值都是指针(地址值)。
动态内存管理
new
操作符可以在运行时为基本数据类型、数组和对象分配持久的内存空间。局部变量是非持久的,当函数返回时,调用栈中的局部变量会被丢弃掉。试图访问指向这样地址的指针,会导致不正确的、不可预知的结果。为修正这个错误,需要为该变量分配持久的内存空间,以便能在函数返回后正常访问它。