C/C++字符串,数组,指针,引用相关知识总结
一.字符串
C语言
-
char s[] = "Hello";
字符数组是最常用,最便捷的字符串定义方式。可以按下标访问,可以修改字符串内容。
s[] = "Hello"相当于把这几个字母存入数组空间中。我们都知道字符串常量会自动在字符串末尾添加字符串终止符’\0’。所以这里就有一个问题,就是我们是否将’\0’也存入了数组中,我们通过以下代码进行测试:
char s[] = "Hello"; printf("%d\n", strlen(s)); //输出结果为5 printf("%d", sizeof(s)/sizeof(s[0])); //输出结果为6
第二个输出结果为6,证明’\0’确实存入了数组中。
按另一种方式赋值时,应明确添加终止符:
char s[] = {'H', 'e', 'l', 'l', 'o', '\0'};
-
char* s = "Hello";
首先说明,这种定义方式本质上是指针指向常量,这在C语言中并不会报错,但在C++中会给出警告:
[Warning] deprecated conversion from string constant to ‘char*’ [-Wwrite-strings]
所以尽量不要用这种方式。
要解除警告可以用const关键字:
const char* s = "Hello";
而这种字符指针的定义方法,也可以按下标访问,但不可以修改字符串的内容,如果强行更改会导致未定义行为。
因为s仅仅是一个指针,并没有分配空间,只是将s指向"Hello"字符串常量的首地址。
对其进行相同的测试:
char* s = "Hello"; printf("%d\n", strlen(s)); //输出结果为5 printf("%d", sizeof(s)/sizeof(s[0])); //输出结果为8
第二个输出结果为什么为8呢?
因为字符数组的定义方式中s是数组名,sizeof(s)得到的是数组空间的字节数;而字符指针的定义方式中s是指针名,在大多数64位计算机中,指针大小是 8 字节,故sizeof(s)得到的仅仅是指针本身占用的内存大小,为8字节。
**注意:**64位计算机中的64位是机器字长,机器字长指的是cpu一次能处理的数据的长度,这也决定了数据总线的长度为64位。指针也是数据,所以指针长度和机器字长最好是相等的。而指针的长度和地址总线又是对应的,所以地址总线的长度一般情况也会等于机器字长。
但是实际中地址总线很多高位用不到,所以默认置0,省去了一些地址总线,总的长度是小于64的,只是64位的机器理论上可寻址的范围在264B。
-
char* s = (char*)malloc(sizeof(char)*6); //C,释放用free(s); char* s = new char(6); //C++,释放用delete[] s;
第一种定义方法是字符数组,数组会自动申请内存空间【局部数组位于栈内存(超出作用域后内存会自动释放),全局数组位于静态存储区(数据段)】。
而这种定义方式是手动申请内存(堆内存),需要手动释放。
需要注意的是,在手动申请内存空间时,不要忘记’\0’也要占一个字节,比如"Hello"需要申请5+1=6个字节的空间。但是有的时候申请5个甚至更少好像也能运行下去,可能是因为malloc 分配的内存可能足够“幸运”,使得写入超出边界的内容没有立即导致明显的错误。
而通过字符数组定义字符串时,若提前规定好数组大小,但小于所需空间大小,会直接报错:
char s[5] = "Hello"; // 错误:5个字符空间不足以容纳6个字符
申请完内存空间后,不能直接通过s = "Hello"赋值,这样只会改变指针的指向,变成第二种情况。
正确的赋值方式应该是:
strcpy(s, "Hello");
进行以下测试:
char* s = (char*)malloc(sizeof(char)*6); strcpy(s, "Hello"); printf("%d\n", strlen(s)); //输出结果为5 printf("%d", sizeof(s)/sizeof(s[0])); //输出结果为8 if(s[5] == '\0') printf("Y"); else printf("N"); //输出Y free(s);
最后还需要说明的是,在C语言中,字符串不能通过s1 = s2的方式赋值 ,因为数组(方式1)没有这种操作;指针(方式2和3)也只是将s1指向s2所指向的内存区域。
也不能通过if(s1 == s2)的方式比较两个字符串,因为数组比较的是数组的首地址,一般来说肯定不相等;而指针比较的是指针的值,也就是指针指向的内存空间的首地址,若s1和s2指向内存中的同一个字符串常量(这取决于编译器),那么则有可能相等,但一般来说也都是不相等的。
正确的赋值方式是strcpy(s1, s2),正确的比较方式是strcmp(a, b)。
C++
C++除了继承了C语言的定义方式之外,还添加了string类,具体用法可以自行参考C++文档。
需要注意的是,我之前在很多地方看到对string类的介绍中有这样一句话:
字符串常量以’\0’作为结束符,但将字符串常量存放到字符串变量中时,只存放字符串本身而不包括’\0’。
但是通过以下代码测试:
string s = "Hello";
if(s[5] == '\0') cout << "Y";
else cout << "N"; //输出Y
证明’\0’被存入了字符串变量中。那应该如何理解这句话呢?
string类并不需要像 C 风格字符串那样依赖 ‘\0’ 来确定字符串的长度,因为它自己内部维护了这个信息。当赋值给string对象时,string仅记录字符串的有效字符('H', 'e', 'l', 'l', 'o'
),并在需要时访问 ‘\0’(比如以上代码),但用户在使用 string时通常不需要直接关注这个结束符。
除此之外,string类字符串复制用赋值号,字符串连接用加号,也可以使用关系运算符直接进行比较。
string s1 = "hello ";
string s2 = "world";
string s3 = s1; //字符串复制
s3 = s1 + s2; //输出s3就是hello world。
**注意1:**在字符串连接时,要连接的字符串中至少有一个是字符串变量。不能全部都是字符串常量。
**注意2:**C++和Java不同,在创建对象的实例时,Java必须使用new手动分配内存空间。而C++可手动,也可自动,不使用new的时候就是自动分配。
指针和引用
指针
-
我们上面对于字符串的解释中多次提到了指针,那么指针到底是什么呢?它又是如何实现”指向“的?
指针是C/C++中一种特殊的变量,它可以存储另一个变量的内存地址,从而**”指向“**另一个变量。
-
那么变量的本质又是什么呢?变量名又是什么?
变量本质上是内存中一块存储空间的抽象,用来存储数据,所以每个数据都有自己对应的内存地址。
一般计算机的最小可寻址单位是字节,也就是按字节编址,每个字节对应一个地址。
比如说int类型的数据占4个字节,每个字节对应1个内存地址,所以1个int类型的数据对应4个内存地址。
当一个指针指向该数据的时候,也就是把该数据的内存地址存入一个指针中的时候,不可能把四个地址都存进去,只会存入变量的首地址。
而变量名本质上是就是变量首地址的一个别名。通过变量名访问数据就是通过首地址+偏移量来实现的(偏移量取决于数据占用的字节数)。
-
对于复合数据类型数组来说,它是否符合以上说法?
答案是肯定的。我们都知道数组名就是数组首地址,所以*a就是a[0],但a[i]不是*(a+i*n) (n是1个基本数据类型所占的字节数),而是*(a+i)。因为编译器会自动计算基本数据类型的偏移量。
对于int a[5]来说,sizeof(a) = 20,而不是5。因为a是整个数组的名称,自然对应整个数组的大小。
**注意:**虽然说变量名是数据地址的一个别名,但对于基本数据类型,要通过变量名得到数据首地址必须使用取地址符&。而数组名却可以直接作为地址使用。举例如下:
int a = 5; int* p = &a;
为什么要将p定义为指针呢?
因为&a的返回值类型就是int*,而且指针本质上就是一个存储地址的数据类型。
引用
引用是C++中引入的概念。和指针不同,引用并不是一个独立的变量,不单独占内存单元。
引用本质上是给变量起一个别名,允许程序员以不同的名字来操作同一个内存地址的变量。其符号和取地址符&相同。如:
int a = 10;
int& b = a;
int& c = b; //b和c都是a的引用,修改任意一个,其他两个也会改变
引用最常见的用法就是作为函数的形参,也可以作为函数的返回值。
需要注意的是,一个函数返回引用类型,必须返回某个类型的变量,而且这个变量必须是全局变量或者静态(static)局部变量。其他的不做赘述。
在C语言中没有引用,但我们也常常进行类似于引用的操作。因此我们可以通过指针模拟引用:
int a = 5;
int* p = &a;
*a = 10;
printf("%d", a); //输出为10
这种方式同样也可以用作函数的形参:
void swap(int* a, int* b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main() {
int m = 3, n = 6;
swap(&m, &n); //实参&m,&n数据类型为int*
printf("%d %d", m, n); //输出6 3
return 0;
}
数组
通过以上的知识,我们对数组应该有了更加深刻的认知。这里再补充一些知识点:
-
a == &a[0]
其实很好理解,数组空间是一段连续的内存单元,无论是数组的首地址,还是a[0]的首地址都是是这段内存单元第一个字节的内存地址,故a == &a[0]。
-
二维数组
二维数组的每一行可以看作一个数组名位a[i]的一维数组,由上一点可得:a[i] = &a[i][0]。