三. 数组、字符串、指针
1. 数组
数组就是一组名为数组元素(或简称元素)的内存位置。各个内存位置存储相同数据类型的数据项。
1.1. 数组声明
类型名 数组名[元素个数N];
数组可以声明称任意类型。
数组的所有元素都存储在连续的内存区域内。
_countof()
可以获取数组元素个数。
1.2. 数组元素引用
数组元素的引用:
数组名[索引值]
数组的索引值可以是任何结果为整数的表达式(0~元素个数N-1)。
当使用非法索引值时,编译或运行不会产生任何警告。
1.3. 数组初始化
类型名 数组名[元素个数N] = {常量1,常量2,......,常量M}; N>=M
如果没有初始化列表,数组元素将都是无效值。
- N > M:第0个元素~第M-1个元素赋给定值;自第M个元素起,后续N-M个元素均被初始化为0;
- N = M:此时可以直接省略元素个数N不写。
1.5. 字符数组和字符串处理
char Array_1 = "Hello World!";
字符串是附加有特殊字符(字符串结尾标志,即’\0’空字符,占用一个字节,8位全为0)的字符序列。
终止符(’\0’)是编译器自动添加的,如果在该字符串字面值中显式添加’\0’,则最终将得到两个空字符。
在指定字符数组的元素个数时,必须考虑到终止字符的存在。
1.6. 多维数组
初始化:
int Array_2[M][N] = {
{}
{}
{}
...
{}
}
多为数组可以省略第一唯的大小,即M;
2. 间接数据访问
2.1. 指针的概念
用来存储数据值的各个内存位置都有地址。地址给PC硬件提供了引用具体数据项的途径。
指针变量存储特定类型的另一个变量的地址。
2.2. 声明指针
类型名* 变量名;
即,指向“类型名”类型的指针。
注:指针的长度
在32位系统中,地址长度为 4 Bytes,即指针长度为 4 Bytes;
在64位系统中,地址长度为 8 Bytes,即指针长度为 8 Bytes;
取址运算符 &
一元运算符,用于获得变量的地址。也可以称为引用运算符。
可以使用&运算符获得任何变量的地址,但需要有适当类型的指针来存储地址。
2.3. 使用指针
2.3.1. 间接寻址运算符 *
将间接寻址运算符*与指针联用,可以访问该指针指向的变量的内容。
该运算符也称为解除引用运算符(引用即寻址符&);而访问指针所指的变量中数据过程称为解除引用指针。
2.3.2. 需要使用指针的原因
- 可以处理数组中的数据,执行速度更快
- 动态地(即程序执行过程中)为变量分配存储空间
2.3.3. 初始化指针
int ivar = 0;
int* ivar_p = &ivar; //int* ivar_p = nullptr;
指针字面值nullptr不指向任何对象。(在C中,使用0和NULL)
nullptr可以隐式(自动)转换为任何指针类型,但不能隐式转换为除bool类型以外的任何整形。
nullptr为false,其他任何指针值都转换为true。
2.3.4. 指向char类型的指针
char* cvar_p = "Hello World!";
"Hello World!"
实际上是一个const char
类型的数组({'H','e','l','l','o',' ','W','o','r','l','d','\0'}
),并将该数组的首地址存储在cvar_p
指针中。
注意:
对于
cout << ivar_p << endl;
cout << cvar_p << endl;
前者输出的仅仅是一个地址,而后则输出的是一串字符。
这是因为输出操作看待“指向char”类型的指针类型的方式,即将其视为字符串。
2.3.5. sizeof
sizeof生成size_t(通常为unsigned int)类型的整数值,给出其操作数占用的字节量。
sizeof 变量名;
当sizeof作用于一个数组名称时,返回整个数组的字节量。
sizeof iarr_1;
sizeof也可以作用与类型名称而不是变量。但在这种情况下,类型名称用括号括起来。
sizeof(int);
区分3种与const、指针及指针指向的对象有关的情况:
- 指向常量对象的指针 const int* ptr
不能修改指向对象
- 指向某个对象的常量指针 int* const ptr
不能修改指针
- 指向常量对象的常量指针 const int* const ptr
均不能修改
注:
const char* ptr_1;
const char* const ptr_2;
前者声明的是指针ptr_1
指向的对象是const
型;而不是指针。
后者声明的才是const
型指针。
2.3.6. 指针与数组
如果单独使用一维数组的名称,则该名称自动转换为指向数组第一个元素的指针。
int* iarr_p = nullptr;
int iarr_1[5];
iarr_p = iarr_1; //iarr_p = &iarr_1[0];
2.3.6.1. 指针处理多维数组
对于多维数组:
int iarr[M][N];
声明一个指针:
int(* iarr_p)[N] = iarr;
是一个指向具有N个数组元素的一维数组的数组指针。
*(*(iarr_p+i)+j) 等价于 iarr[i][j]
3. 动态内存分配
在程序执行期间,经常需要根据输入的数据,来决定应该给存储不同类型的变量分配空间量。
3.1. 堆——空闲存储器
大多数情况下,执行程序时,计算机有部分未使用的内存。这些内存在C++中称为堆(空闲存储区)。
3.2. new & delete操作符
new操作符:
double* dvar_p = nullptr; //首先定义一个指针
dvar_p = new double; //new操作符返回堆中分配给后接类型(此处为double)的地址
*dvar_p = 1.0; //在该地址存储一个double变量
如果堆中内存已用尽或者没有足够的连续字节提供给需要获得空间的变量(如double型为8个字节,如果堆中有空间但最大连续字节为7),则返回一个异常,终止程序。
new创建的变量也可以初始化,但是不能用“=”,需要用“()”形式:
dvar_p = new double(1.0); //分配内存与初始化同时进行
更进一步,可以将上述三条语句合并在一起:
double* dvar_p(new double(1.0));
delete操作符:
当不再需要某个变量时,利用delete释放内存:
delete dvar_p;
在释放指针指向的内存时,还应该把指针的值设置为nullptr。
内存泄漏:
如果不是用delete,随后又在dvar_p中存入一个不同的地址值(是改变dvar_p的值,而不是改变*davr_p的值),那么将无法再释放这块内存,或使用其包含的变量,因为失去了访问该地址的途径。
3.3. 为数组动态分配内存
char* carr_p = nullptr;
carr_p = new char[n]; //n为数组元素个数,自定,首地址赋给指针
...
delete [] carr_p; //“[]”表明所需删除的是一个数组
carr_p = nullptr;
不能在动态分配内存过程中给数组初始化。
3.4. 多维数组动态分配内存
char (*carr_p)[m] = nullptr;
carr_p = new char[n][m]; //首地址赋给指针,这里m,n都是常量
...
delete [] carr_p; //“[]”表明所需删除的是一个数组,无论维数多少
carr_p = nullptr;
在new分配中,可以使用变量来指定数组最左边那一维。
4. 使用引用
4.1. 引用的概念
引用分两种类型:
- lvalue
- rvalue
实质上,引用是可用作其他对象的别名的一个名称。
lvalue
引用的是一个可出现在赋值操作左边的持久存储位置;
lvalue
引用是另一个变量的别名;是别名而非指针,声明引用时必须指出对应的变量;不能修改引用,使其表示另一个变量。
rvalue
实质上是一个暂存的临时值。
4.2. 声明并初始化lvalue
引用
int a = 0;
int& ref_a = a; //声明一个lvalue引用,该引用初始值为a变量名;
//之后可以使用ref_a代替原来的变量名,共享内存
//变量ref_a 属于“指向int的引用”;与指针的初始化类似
对于常量:
const int& ref_Data = 5; //必须添加const限定
指针与引用之间最大区别:指针需要解除引用才能访问其指向的变量,而引用不需要(引用不能改为引用别的变量)。
lvalue
引用完全等价于被引用的变量。
4.4. rvalue
引用
此处略微介绍,后补全。
C++中每个表达式要么是lvalue
,要么是rvalue
。
变量是lvalue
,一个内存位置。
rvalue
引用是对包含表达式结果的内存位置的引用。
int x = 5;
int&& ref_exp = 2*x+3; //rvalue引用,引用2x+3求值结果,是一个临时值
cout << ref_exp << endl;
int& ref_x = x; //lvalue引用
cout << ref_x << endl;
4.5. 字符串库函数 string.h
4.5.1. 以空字符结尾的字符串长度
strlen()
将char*
类型的参数字符串的长度(不包含结尾的空字符)作为一个size_t
类型的值。
wcslen()
则针对wchar_t*
类型的字符串。
strlen()
与wcslen()
都是以字符串结尾的空字符来确定长度。如果没有空字符,函数会继续检查整个内存。
4.5.2. 以连续空字符结尾的字符串
strcat(str1,str2)
函数用来拼接两个以空字符结尾的字符串,第二个字符串首字符覆盖第一个字符串的空字符。第一个字符数组必须有足够的空间容纳两个数组。strcat(str1,str2)
返回首字符地址。
wcscat(str1,str2)
用于宽字符。
strncat(str1,str2,n)
功能同strcat(str1,str2)
,但第三个参数指定第二个字符串要拼接的字符数量。并在结尾自动加上一个空字符。
wcsncat(str1,str2,n)
用于宽字符。
由于上述介绍函数都是以寻找空字符来实现的,这是不安全的。
在cstring.h
中,strcat_s(str1,m,str2)、wcscat_s(str1,m,str2)、strncat_s(str1,m,str2)、wcsncat_s(str1,m,str2)
提供了安全的方法。m
为str1
数组大小。
如果在拼接过程中出现错误(源/目标字符串不存在、目标字符数组容量不足),将产生错误,而且目标字符数组不会发生改变。
4.5.3. 复制以空字符结尾的字符串
strcpy(str1,str2)
将str2复制到str1中覆盖。
strcpy_s(str1,m,str2)
4.5.4. 比较以空字符结尾的字符串
strcmp(str1,str2)
第一对不同的字符确定了第一个字符串是小于/大于第二个字符串。
小于,则返回负值
等于,则返回零
大于,则返回正值
4.5.5. 搜索以空字符结尾的字符串
strspn(str,set)
在搜索str中不包含在set中的第一个字符,并返回索引(不是地址)。
char* str = "I am idot";
char* vowel = "aeiouAEIOU ";
unsigned char index = strspn(str,vowel);
其中'I'、' '、'a'
均可在vowel
中搜索到,而'm'
不再vowel
中,所以函数返回3
;
strstr(str1,str2)
返回一个指针,指向str1
中str2
指定的子字符串的位置。
char* str1 = "Jones had had \" had had\" had had \"had\"."
char* str2 = "had";
char* ptr = nullptr;
ptr = strstr(str1,str2);