C语言补漏:字符串指针与字符数组传参

字符串指针与字符数组传参

深信服的笔试上被吊打,其中对一道用指针做形参的题目印象十分深刻,借此恶补了一晚上指针,今天总结,以作警示。

​ 试想有如下情形,将一个字符串指针做形参赋值函数修改其字符串,函数结束后字符串被改变了吗?

#include<stdio.h>
void testPstr(char *ppstr){
    ppstr = "hasten";
    printf("%s\n",ppstr);
}



int main(){
    char *pstr = "test";
    printf("%s\n",pstr);
    testPstr(pstr);
    printf("%s\n",pstr);  // test ? hasten?
}

结果是没有。

test
hasten
test

我以前天真的以为指针本质上就是地址,把指针传递给函数其实就是把地址传递给函数来操作其实不然,这里编译器的逻辑是这样,pstr本质上是main()函数内的一个局部变量,而函数显然是不能跨函数使用别的函数的变量的(显然这样做会有安全问题),怎么办呢,那就是在函数中拷贝一个同样的变量,接受实参的值来进行操作。

对于指针来说,它的值是地址,传参本质上是函数内定义了一个新指针(这里是ppstr)指向同一个地址,但是和int, double, char这类整形变量的传递不一样的是,char *指向的是一个字符数组,该字符数组存储在程序的.rodata段,也就是存放只读数据的区域,这就是为什么直接修改char *引用的字符串会导致SF:

    char *pstr = "test";
    pstr[0] = 'Z';  // segment fault
    printf("%s\n",pstr);

程序不可能让你修改这段内存的数据,因此ppstr = "hasten" 这段操作本质上是让一个与main毫无关联的指针ppstr从指向rodata内和pstr的一样的内存段, 转为指向rodata的另一个存储着"hasten"字符串的区域, 最后函数结束时ppstr在栈中被销毁,不会影响到main() 中的pstr。

pstr->"test"<-ppstr

pstr->"test"
ppstr->"hasten"

验证如下:

#include<stdio.h>
void testPstr(char *ppstr){
    printf("ppstr_cont:%s ppstr_cont_addr:%#x ppstr_addr:%#x\n",ppstr,ppstr,&ppstr);
    ppstr = "hasten";
    printf("ppstr_cont:%s ppstr_cont_addr:%#x ppstr_addr:%#x\n",ppstr,ppstr,&ppstr);

}

int main(){
    char *pstr = "test";
    printf("pstr_cont:%s pstr_cont_addr:%#x pstr_addr:%#x\n",pstr,pstr,&pstr);
    testPstr(pstr);
    printf("pstr_cont:%s pstr_cont_addr:%#x pstr_addr:%#x\n",pstr,pstr,&pstr);
}

结果:

pstr_cont:test pstr_cont_addr:0x404039 pstr_addr:0x61fe18
ppstr_cont:test ppstr_cont_addr:0x404039 ppstr_addr:0x61fdf0
ppstr_cont:hasten ppstr_cont_addr:0x404032 ppstr_addr:0x61fdf0
pstr_cont:test pstr_cont_addr:0x404039 pstr_addr:0x61fe18

我这里操作环境是Windows, 之前试过linux,但是每次运行地址的变动相比win要大,不好验证,就改到windows上了,但本质都差不多。

可以看到地址中404开头的是常量存储区,61f开头的是栈区,pstr指向的字符串test的首字符t存储在0x404039,其指针本身地址在0x61fe18, 传入函数以后另拷贝的一个指针ppstr地址在61fdf0,原本也是指向test首字符所在的地址0x404039, 后来指向的hasten首字符t存储在0x404000,自此和pstr不相及。

那要怎么把修改值赋值给pstr呢?方法是用二级指针:

#include<stdio.h>
void testPstr(char **ppstr){
    printf("ppstr_cont:%s ppstr_cont_addr:%#x ppstr_addr:%#x\n",*ppstr,ppstr,&ppstr);
    *ppstr = "hasten";
    printf("ppstr_cont:%s ppstr_cont_addr:%#x ppstr_addr:%#x\n",*ppstr,ppstr,&ppstr);

}

int main(){
    char *pstr = "test";
    printf("pstr_cont:%s pstr_cont_addr:%#x pstr_addr:%#x\n",pstr,pstr,&pstr);
    testPstr(&pstr);
    printf("pstr_cont:%s pstr_cont_addr:%#x pstr_addr:%#x\n",pstr,pstr,&pstr);
}

这样传过去的就是pstr的地址了,函数内的ppstr是指向pstr的,右值调用这些指针实际指向的内存如下,具体结果就不放了,大家可以实际验证一下:

入函数前内存:	ppstr->pstr->"test"
入函数后内存:	 ppstr->pstr->"ppstr"
引用方式:    &ppstr ppstr  *ppstr
				   &pstr   pstr
	

另一个方法就是把testPstr改为返回char*指针的函数,就不多说了。

C语言指针难就在于,它的语法规范多(*,&),结合左值引用和右值引用, 整体使用就变得较为繁琐,同样的指针语法,典型的例子就是链表(p->next作为左值表示要指向的地址,p->next作为右值表示p正指向的地址),平时使用时一定要多加谨慎。

再回到上图的例子,如果把char *pstr 改为 char pstr[] , 函数的形参从char * 改为 char [] ,会如何?

void testPArr(char pstrArr[]){
    pstrArr[0] = 'Z';
    printf("pstrArr in test:   pstrArr_cont:%s pstrArr_cont_addr:%#X pstrArr_addr:%#X\n\n", pstrArr, pstrArr, &pstrArr);
}

int main(){

    //连续类型
    char pstr[] = "test";
    printf("pstr in main:   pstr_cont:%s pstr_cont_addr:%#X, pstr_addr:%#X\n\n",pstr,pstr,&pstr);
    testPArr(pstr);
    printf("pstr in main:   pstr_cont:%s pstr_cont_addr:%#X, pstr_addr:%#X\n\n",pstr,pstr,&pstr);

}
pstr in main:   pstr_cont:test pstr_cont_addr:0X61FE1B, pstr_addr:0X61FE1B

pstrArr in test:   pstrArr_cont:Zest pstrArr_cont_addr:0X61FE1B pstrArr_addr:0X61FDF0

pstr in main:   pstr_cont:Zest pstr_cont_addr:0X61FE1B, pstr_addr:0X61FE1B

可以看到在函数中对字符数组的修改就是对pstr原本的地址操作的, 原因是pstr这时候是一个存储了内容为“test"的字符数组,其所有数据都在用户栈区,故可以被修改,当其作为函数参数的时候,编译器会把它解析成一个指向其首元素首地址的指针,void testPArr(char pstrArr[]) 相当于void testPArr(char *pstrArr), 运行结果中也可以看到其有独立的指针地址,是一个指向字符数组的指针。通过指针下标引用所做的修改都会影响到字符数组。

但本质上,函数仍旧是对传入的数据拷贝了仅在函数内作用的指针,故在上述程序若让 pstrArr = "Hasten" 的话还是让指针放弃原来指向的来自用户栈区的字符数组,而指向另一个来自.rodata段的字符串字面量,跟pstr分道扬镳。

如果要修改字符数组内的内容怎么办? 显然无法让pstr = "xxx", 因为pstr是已经分配好数据的数组,是这段数据的标头,不是指针,无法直接引用字符串字面量。唯一的方法是用循环将字符一个个拷贝其分配内存中。

char pstr[] = "test";
char data[] = "data";
pstr = data; //error
for(int i = 0; i < sizeof(data)/sizeof(data[0]); i++){
	pstr[i] = data[i];
}

显然字符数组和字符指针各有其的特点,比如如果想设置一段字符串的访问属性为只读时就可以用const char *, 而如果想读写就可以用char x[]

总结:

  • 函数无法直接使用别的函数的变量,而是将实参变量拷贝一份,因此可以让函数拷贝指针形参,指向实参的地址就可以对实参变量所在地址的值做修改

  • 字符数组和字符指针的性质不同,字符数组的数据就是数组本身,属于栈段,可以修改,而字符指针是引用来自.rodata段的字符串,修改其引用的字符串会导致程序错误(也就是为什么实践时凡是使用char*都一定定义为const char*

  • C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。

参考

《C和指针》 指针部分

C语言数组参数与指针参数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值