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

2023-11-16

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

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

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

#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语言数组参数与指针参数

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言补漏:字符串指针与字符数组传参 的相关文章

随机推荐

  • minikube单机安装nfs服务

    1 安装 nfs server sudo apt get update sudo apt get install y nfs kernel server 2 创建目录 配置 IP 共享目录绑定 vim etc exports 新增 data
  • Shiro权限框架-实现分布式会话SessionManager(7)

    1 会话的问题 2 分布式会话实现思路 1 原理分析 所有服务器的session信息都存储到了同一个Redis集群中 即所有的服务都将 Session 的信息存储到 Redis 集群中 无论是对 Session 的注销 更新都会同步到集群中
  • vue+elementUI图片预览,<el-image> 的使用

    vue elementUI图片预览 el image 的使用 本文转载自 https www cnblogs com allanlau p 13397625 html 首页定义data data return imgs imgsVisibl
  • 手把手教你使用transciver-ip核的配置

    目前很多行业都会用到transceiver 甚至像pcie srio等高速接口都调用了transceiver 所以了解并学会其使用方法还是很重要的 本文结合作者的使用经验 让你快速的了解并上手使用 Xilinx提供了Transceivers
  • (Scikit-Learn)朴素贝叶斯使用方法:高斯朴素贝叶斯 多项式朴素贝叶斯(文本分类)

    在贝叶斯分类中 我们希望确定一个具有某些特征的样本属于某类标签的概率 通常记为 P L 特征 贝叶斯定理告诉我们 可以直接用下面的公式计算这个概率 假如需要确定两种标签 定义为 L1 和 L2 一种方法就是计算这两个标签的后验概率的比值 其
  • Python自学之路第九步——用户输入和while循环

    主要用到的是input 函数 他可以接受用户的输入 这样便可以编写交互式的程序了 还介绍了while循环 这个和C中一样 包括if判断都可以尝试测试一下 很有意思 函数input pr 我们将统计您的基本信息 pr n请输入您的名字 nam
  • Mybatis - NoSuchMethodError: net.sf.jsqlparser.statement.select.SetOperationList.getSelects()Ljava/

    昨天在修改一个接口功能时 需要在原来的接口上提供分页和模糊查询 就使用了分页 PageHelper来做 但是在mybatis的xml文件中又使用了UNION来合并查询结果 导致项目启动直接报错 Handler processing fail
  • MySQL 中读写分离可能遇到的问题

    前言 MySQL 中读写分离是经常用到了的架构了 通过读写分离实现横向扩展的能力 写入和更新操作在源服务器上进行 从服务器中进行数据的读取操作 通过增大从服务器的个数 能够极大的增强数据库的读取能力 MySQL 中的高可用架构越已经呈现出越
  • ubuntu 光盘读取

    把光盘放入光驱后 要挂载光驱 将光驱设备挂在到 mnt 下 sudo mount dev sr0 mnt mount dev sr0 is write protected mounting read only 到 mnt目录下就可以看到光盘
  • 一条SQL语句求前面记录的平均值

    有算法要求如下 For i 1 i lt 10 i ta i t 1 t 2 t i i 用一条SQL语句实现它 分别用表变量 ta 和 t 来对应 ta 和 t declare t table id int d decimal 18 4
  • 第一课 认识Python

    相比大家都听说过Python是一门容易学习的语言 那么实际是怎么样呢 我们从以下几点看看 1 首先 Python是最容易学习和最好用的语言 Python容易阅读和编写 比较清晰 格式非常简洁 表达能力强 这样同样写一个程序 Python比其
  • uniApp条件编译以及跳转方法

    1 使用Uniapp的方法获取系统环境 仅在JS中可以使用 uni getSystemInfoSync platform 获取应用所在的平台 if uni getSystemInfoSync platform ios if uni getS
  • pickle与.pkl文件

    经常遇到在Python程序运行中得到了一些字符串 列表 字典等数据 想要保存下来 长长久久的 方便以后使用 这个时候Pickle模块就派上用场了 pickle 模块及其同类模块 cPickle 向 Python 提供了 pickle 支持
  • mipi介绍

    文章目录 1 MIPI简介 1 1 DSI layer 2 D PHY 2 1 D PHY介绍 2 2 电平状态 2 3 lane结构 2 4 data lane操作模式 2 4 1 escape mode和space one hot co
  • kylin启动netstat: n: unknown or uninstrumented protocol

    检查hadoop配置的时候出现问题 报错如下 lcc lcc apache kylin 2 6 0 hbase1x bin check env sh Retrieving hadoop conf dir KYLIN HOME is set
  • 史上最快的实例分割SparseInst Int8量化实录

    近期 YOLOv7里面借鉴 复 制 粘 贴 了一个新的模型 SparseInst 我借助YOLOv7的基建能力 将其导出到了ONNX 获得了一个非常不错的可以直接用OnnxRuntime 或者TensorRT跑的实例分割 后续也可能把lin
  • 数据挖掘十大算法

    参考 ICDM 数据挖掘十大算法
  • flutter 去除超出警报

    给超出内容套上SingleChildScrollView组件即可 SingleChildScrollView child
  • wpf datagrid自动生成列时特殊字符转换

    DataGrid控件可以根据DataTable自动生成行和列 但是如果列名包括一些特殊字符 的时候 会出现无法显示出数据或者显示DataRowView的情况 原因是这些字符是Xaml里用来标识绑定path和xpath的符号 例如我们会这么用
  • C语言补漏:字符串指针与字符数组传参

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