C/C++字符串,数组,指针,引用相关知识总结

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]。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sabrisimba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值