0%

字符数组,字符串与数组名

基础不牢,地动山摇。当初学C语言的时候,指针,数组等概念一直分不清楚,十分混乱。后期字符串与字符数组的出现更是云里雾里。现在学了C++,加上一些C++11后的特性数组,对这玩意的用法更加迷惑,时而&arr时而&arr[0]。今日来做个了结。

字符与ASC码

我们知道在计算机内部,所有信息最终都是一个二进制值,这称为数字数据。而字符数据通常称为文本,如记事本文件,word文件等。计算机使用多种类型的编码方式来展示字符数据。其中之一的编码方式为ASC码,使用8个bit位来表示一个字符。所以,在计算机设备上显示的文档是经过ASC,Unicode或UTF-8编码后的一串二进制数字。我们可以通过以下程序来观察字符A的二进制:

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

int main(){
int a{65};
// 十进制数字65 对应的 ASC 字符
std::cout << static_cast<char>(a) << std::endl;

char b{'b'};
// 字符对应的十进制数字
std::cout << (static_cast<int>(b)) << " ";
// 数字的二进制表示
std::cout << std::bitset<8>(static_cast<int>(b)) << std::endl;
return 0;
}

字符数组

在C语言中没有专门的字符串变量,通常用一个字符数组来存放一个字符串,且以’\0’作为串的结束符。在C语言中,它有多种初始化方法:

1
2
3
char c[]{'a', 'b', 'c', 'c', '\0'};
char c1[] = {"abcd"};
char c2[] = "asdasd";

这里需要注意的是,C语言中数组名表示该数组的首地址,整个数组是以首地址开头的一块连续的内存单元。也就是,对于字符数组c而言,若从键盘读入,scanf("%s", &c)是错误的,不应该取地址:scanf("%s", c)。在执行输出函数时,按数组c找到首地址,然后逐个输出数组中各个字符直到遇到字符串终止标志\0为止。自己不添加终止符时,编译器会帮你添加,但最好自己添加。

我们可以使用格式化字符串%s整体输出字符数组,这样不会尴尬的输出数组的首地址。但对于非字符型数组而言,输出的就是数组首地址了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main(){
char c[]{'a', 'b', 'c', 'c', '\0'};
printf("%s\n", c); // 打印字符数组
std::cout << c << std::endl; // 打印字符数组
printf("%c\n", c[0]); // 打印第一个字符

int a[]{1, 2, 3, 4, 5};
printf("%d\n", a); // 打印数组首地址
printf("%d\n", a[0]); // 打印第一个元素

return 0;
}

整型数组转字符数组整体输出

看了上面的代码,既然输出整型数组的数组名时,输出的是地址。总所周知数组名是指针,那么把int*的指针强制改为char*的指针会出现什么情况?这里需要注意的是:int占4个字节,char占一个字节,强制类型转化时,char类型的开头会是0,也就是C语言中字符数组的结尾。所以以下程序是没有反应的:

1
2
3
4
5
6
7
#include <iostream>

int main(){
int a[]{1, 2, 3, 4, 5};
printf("%s", reinterpret_cast<char*>(a));
return 0;
}

看下内存就懂了:

此时突发奇想,如果一定要按照%s格式的方法输出整型数组里面的全部内容,该如何实现呢?先补充点额外知识,假设有一个数据单元,长度是$n$个bit,那么两个数据单元就是$2n$个比特。如果让这两个数据单元内的数值一致,假设想要两个数据单元里面存储的都是$x$,那么写入的数据应该是:$x\times 2^n+x$。

如下图所示,长度为4的两个数据单元写入170时,两个数据单元内都是10。

这样,使用uint_16(两个字节)类型转化为char类型是,就容易控制些了。因为256*65+65=16705,来个一次性输出整型数组内容的操作:

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
const uint16_t a[] = {16705, 16705, 0};
printf("%s\n", reinterpret_cast<const char*>(a));
// 输出 AAAA
return 0;
}

这样,就不会在有0x0的存在打断输出了,且,整型的65会转换为char类型的A输出。

数组与指针

指针

我们接着往下看,虽然初学指针令人头疼,但掌握指针的确会对程序以及内存有更好的理解。内存可以看成一系列连续编址的单元,而指针是能存放地址的一组单元。假设指针p指向数据c,那么就p = &c,可以画图为:

可以使用一元运算符*来间接寻址,找到地址存储的内从,可以通过简单的程序来观察指针如何使用:

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
int a = 10;
int* p = &a;
*p -= 9;
printf("%d", a);
}

在复杂一点点,可以通过传递参数的地址,来避免临时变量形参带来的问题,以交换数据为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

void swap(int* tmp1, int* tmp2){
int tmp;
tmp = *tmp1;
*tmp1 = *tmp2;
*tmp2 = tmp;
}

int main() {
int a = 10, b = 16;
swap(&a, &b);
printf("%d %d", a, b);
}

数组

假设此时定义的数组为int a[10],指针为int* pa = &a[0],那么指针pa指向了数组a的第0个元素,也就是说,pa的值为数组元素a[0]的地址。因为数组名所代表的就是数组最开始的一个元素的地址,所以pa = &a[0]pa = a等价。

  • 数组引用的形式a[i]*(a + i)也是一样的,表示取数组a的第i个元素
  • &a[i]a + i的含义也是相同的,表示数组a的第i个元素的地址
  • pa[i]*(pa + i)也是一样的,表示取数组a的第i个元素
  • 两者之间的不同之处在于,指针是一个变量,所以pa++是可以的,但a++是不行的,因为数组名不是变量。

所以:如果在调用子函数时传递数组名,实际传递的是一个地址,因此子函数的形参应该为指针类型,假设写一个求字符串长度的函数:

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

// 参数类型为指针
int strlen(char* s){
int len = 0;
for (len; *s != '\0'; s++){
len ++;
}
return len;
}

int main(){
char s[] = "as 0 as";
int len = strlen(s);
printf("%d", len);
return 0;
}

C++ 风格的数组

在使用C++的array数组时需要注意的是,假设生命的数组为:std::array a{1, 2, 3, 4, 5};,那么&a表示取出a这个数组对象的地址,&a[0]才是取出存储数组位置的首地址。看程序:

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

using std::array;

int main(){
array a{1, 2, 3, 4, 5};
// 地址
std::cout << &a << std::endl;
// 第三个元素
std::cout << *(&a[0] + 2) << std::endl;

int* p = &a[0];
for (int i = 0; i < a.size(); i++){
std::cout << *(p + i) << " ";
}

return 0;
}

C++引用

引用相当于给变量起了个外号,引用附着在存在的变量上。对引用做的读写操作,作用在原来变量上,所以引用在定义的时候就必须被初始化,否则引用不存在。

当引用作为参数传递时,在被调函数中,改变引用参数值,会改变实际参数的值。可以查看上文的交换变量的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void swap_refer(int& tmp1, int& tmp2){
int tmp3 = 0;
tmp3 = tmp1;
tmp1 = tmp2;
tmp2 = tmp3;
}

void swap(int c, int d){
int a = 0;
a = c;
c = d;
d = a;
}

int a = 1, b = 2;
swap(a, b); // 不会交换
swap_refer(a, b); // 会交换
swap_refer(a, 4); // 错误,引用没有可依赖的变量

如果想要使swap_refer(a, 4)不报错,且函数的参数仍然是引用,这时需要引入常量:

1
2
3
4
int add(int& tmp1, const int& tmp2){
return tmp1 + tmp2;
}
swap_refer(a, 4); // 正确 常量左值引用,可以绑定在右值上。

使用引用或指针的优势是:避免传参过程中的变量拷贝带来额外的开销。

感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章