字符串指针与字符数组传参
深信服的笔试上被吊打,其中对一道用指针做形参的题目印象十分深刻,借此恶补了一晚上指针,今天总结,以作警示。
试想有如下情形,将一个字符串指针做形参赋值函数修改其字符串,函数结束后字符串被改变了吗?
#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和指针》 指针部分