实用指南:【C语言】char * 、char [ ]、const char * 和 void *的使用以及区别

news/2025/10/3 17:46:09/文章来源:https://www.cnblogs.com/wzzkaifa/p/19124805

文章目录

  • 一、字符指针char *
    • (一)初始化
    • (二)赋值
  • 二、字符数组 char []
    • (一)初始化
    • (二)字符数组不能赋值
  • 三、字符串常量 const char *
    • (一)const 关键字
    • (二)const char * 指向的数据不允许更改
    • (三)char * const 指针不允许修改
  • 四、void *类型的强大
    • (一)转换
      • 1.void *类型转换的主要作用
      • 2. 函数指针不可以 void *转换
        • (1)传指向函数指针的指针
        • (2)用结构体包裹

一、字符指针char *

(一)初始化

    字符指针初始化可以先声明再初始化,也可以声明即初始化

int main()
{
//先声明, 再初始化
char *p;
p = "hello world";
//char * p= "hello world";//声明即初始化
}

    声明一个字符指针p,将其初始化为 "hello world"
    需要注意的是,编译这两行代码编译器会报警告:禁止将字符串常量转换为char *类型数据
在这里插入图片描述    因为"hello world"是一个字符串常量const char *类型,当编译器编译到这行代码时,首先将"hello world"存储在内存的常量区,然后在存放的时候自动在其末尾加上'\0',最后将存储"hello world"的内存首地址返回。
    由于字符串常量"hello world"是只读的,不可以修改,但是char *类型是可以修改其内容的,因此编译器不确定在未来的某个时刻使用者是不是会更改它,就报送警告以此来提醒使用者。

char * p = "hello world";
//const数据只读不可修改
p[0] = 'H';//erro: Segmentation fault (core dumped)
char *str;
strcpy(str, "other");
str[0] = 'O';//正确,操作的是char *类型数据
printf("%s\n", str);//输出结果为Other

    对char *类型调用数组下标操作符[]是安全的,不安全的地方在操作字符串常量"hello world"上。操作只读的数据会被硬件拦截,触发段错误。
    扩展:段错误,是最常见的内存错误。主要产生原因有:
        1.解引用空指针或未初始化的指针。
        2.访问已释放的内存。
        3.数组越界访问
        4.修改字符串常量

(二)赋值

char *类型可以被赋值。

char * p  = "hello world";
char * str = "str";
char * str1 = "str1"
p  = str;//将str赋值给p
p = str1;//再将str1赋值给p,打印p的值将会是"str1"
//当然,前面说过上述初始化操作会报警告,这是最标准的写法
const char * p = "hello world";
const char * str = "str";
p = str;
printf("%s\n", p);//输出结果是str

二、字符数组 char []

(一)初始化

    字符数组初始化主要有两种:字符串字面量初始化字符数组列表初始化

//字符串字面量初始化, 最常用的写法
char p[] = "hello";//缺省字符数组长度, 编译器会自动判断大小:数组大小为6, 因为字符串会自动添加'\0'
char p[6] = "hello";//正确, 预留'\0'字符的空间
//字符串字面量列表初始化,列表初始化符{}有些多余, 但仍是合法的
char p[] = {"hello"};
char p[6] = {"hello"};
//字符数组列表初始化,该初始化不会自动添加空字符'\0',因此不用预留多一个字符的空间
char p[] = {'h', 'e', 'l', 'l', 'o'};//编译器自动判断大小,数组大小为5
char p[5] = {'h', 'e', 'l', 'l', 'o'};//正确, 但不能用于字符串操作
char p[6] = {'h', 'e', 'l', 'l', 'o'};//正确, p[5]会被零初始化为'\0',因此可以用于字符串操作
//以上写法完全等效,且完全正确,既不会报错也不会报警告

字符串字面量初始化会自动处理终止符'\0',因此在显式声明大小时,一定要预留终止符的空间。如char p[5] = "hello",没有给字符串字面量预留'\0'的空间,因此就会报错。
在这里插入图片描述
字符数组列表初始化不会自动处理终止符,不能用于字符串处理。如果用于字符串,必须要手动添加'\0':

char p[5] = {'h', 'e', 'l', 'l', 'o'};
printf("%c\n", p[1]);//正确, 输出e
printf("%s\n", p);//error
//如果想正确输出为字符串%s
char p1[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", p1);//正确, 输出hello

    注意,当你运行这个实例时,大概率你看到标记error那行代码的输出是正常的但这不意味着这行代码是正确的
    导致该行代码正常输出,程序没有崩溃的原因:一、编译器优化。二、栈内存分配该数组时,在某个地方遇到了'\0'导致printf正常输出。

(二)字符数组不能赋值

char p[6];
p = "hello";//赋值操作, error

    编译时,程序会报错。因此字符数组不能赋值,只能直接初始化
在这里插入图片描述
    或者通过字符串函数,对字符数组进行拷贝操作。

#include <stdio.h>#include <cstring>//strcpyint main(){char p[6];strcpy(p, "hello");printf("%s\n", p);return 0;}

    对于小白来说,可能有点没反应过来,为什么字符数组不可以赋值呢?首先,数组名p是一个常量,是数组的首地址(如0x7FFC5955F7F2)。因此调用p就是调用一个常量。(记住,p是常量,值是一个地址,但整个数组的内存是存放在栈区的,数组名p不代表整个数组,只代表了数组首地址)
    而"hello"也是一个常量,是字符串常量,因此调用"hello"实际上是操作的这个常量的地址(由于是常量,所以该地址位于内存的数据段常量区,只读不可修改,如0x5A93CDF87008)。
    那么大家现在就显而易见了,将一个地址(常量)赋值给另一个常量,是不被允许的,右值是不能被赋值的。只有左值才可以被赋值(如char * p; p = "hello";//这里的p是指针,是变量,也就是左值)。

三、字符串常量 const char *

(一)const 关键字

const用来指定常量或者限制某些变量、函数或指针的修改性。它的功能是告诉编译器某些对象在程序运行过程中不应该被修改数据内容,从而提高该变量的安全性和可维护性。
    常量数据存储在内存的数据段中的.rodata区。
扩展:Linux操作系统将物理内存映射为虚拟内存。所以我们的进程使用的都是虚拟内存。虚拟内存分为栈区堆区数据段代码段
    其中:
        1.栈区存放进程执行过程中局部变量,内存最大限度为8MB;
        2.堆区存放进程执行过程中的自由内存,由用户调用malloc等函数自由操作,堆区内存大小没有限制,最大内存与实际物理内存的大小相关联。
        3.数据段地址从高到低分为三部分:.bss段,.data段和.rodata段。其中,.bss段用来存放未初始化的静态数据(static和全局变量);.data段用来存放已经初始化的静态数据(static和全局变量),因此我们通常将.bss段和.data段统称为静态区.rodata段用来存放常量数据(const),我们通常称为常量区
        4.代码段地址从高到低分为两部分:.text段和.init段。其中,.text段用来存放用户程序的代码,即包括main函数在内的所有用户自定义函数代码,因此也被称为正文段.init段用来存放系统给每一个可执行程序自动添加的“初始化”代码,这部分代码包括环境变量的准备,命令行参数的组织和传递等,并且将这部分数据放在了栈底。
在这里插入图片描述

    最后,如果没有必要,不应该滥用静态变量和全局变量:一是因为这些数据的生命周期是整个进程运行期间,即便你声明后不再调用,它的资源依旧存在。二是静态数据是共享资源,如果进程开了多个线程去访问静态数据,若用户编写代码逻辑不当,就会导致“竞态”。因此在多线程中使用共享资源一定要采用合理的互斥手段(比如)去防止“竞态”。

(二)const char * 指向的数据不允许更改

const char *类型是一个指向字符串常量的指针。
    例如const char * p,指针p指向一块字符串常量内存。一旦初始化后,这块内存存放的数据就不允许更改了。

//声明即初始化
const char * p = "hello world";//非常完美的声明即初始化。比 char * p = "hello world" 更正确更安全更清晰
//不允许赋值
const char * str;
str = "hello world";//初始化
str[0] = 'H';//error: assignment of read-only location '*(const char*)str'

    在const char * p中,指针p是可以指向另一块内存的。const关键字只限制了初始化时的字符串(右值,如例子中"hello world")不能更改其内容,不是限制的指针p(左值)。

const char * p = "hello world";
p = "other string";//正确,可以修改指针p的指向

    这里,"hello world"这个字符串常量的内存在程序运行期间都是存在的,通过阅读上文,就知道"hello world"是存放在数据段的.rodata区,只会在程序运行结束后由内存自动释放。

(三)char * const 指针不允许修改

    如果将const关键字放在后面,const修饰的就是左值指针p,而不再是右值字符串常量了。因此指针p存放的地址不允许修改。让指针p指向另一块内存的地址(即"other string"的地址)是不被允许的。

char * const p = "hello world";
p = "other string";//error:assignment of read-only variable ‘p’

    当然,由于"hello world"是字符串常量,所以上述代码最正确的声明应该是const char * const p = "hello world"

四、void *类型的强大

void *类型是指向未知对象类型的指针,能保存任何对象类型的地址,但不能解引用,不能做指针算术,也不能直接delete

(一)转换

任意类型对象指针转换成void *类型总是安全的。触发的是隐式转换

//将字符数组退化成void *类型
char p[] = "hello world";
void *vptr = p;
//将结构体对象指针退化成void *类型
typedef struct
{
int a;
char b;
}Person;
Person p;
void *vptr1 = &p;

    当我们将任意类型指针转换成void *总是安全的,但我们再将void *转换回去时,必须得是原对象类型。触发的是显式转换
扩展:隐式转换是编译器优化时自动进行的转换;而显式转换需要我们手动转换,即我们平时说的强制转换。

char p[]  = "hello";
void *vptr = p;
char *tmp = (char *)vptr;//必须原样转换回去

1.void *类型转换的主要作用

    大家可能疑惑,这转来转去有什么作用呢?首先,void *类型指向的是未知对象类型,因此不能解引用不能做指针算术不能直接delete(c++)。
    对于POSIX标准线程库(如在Linux系统上),pthread_create()函数中的线程任务函数的参数就是void *,当我们将char *类型作为参数传进去时,就会触发隐式转换,将char *类型转换成void *
    但我们在线程任务函数中使用参数void *arg时,就必须将其转回原来的类型才能使用。

void *func(void *arg)//这是任务函数的实现, 参数类型和返回值类型都是标准规定好了的
{
//1.C/C++支持显式转换, 但C++不支持隐式转换
char *tmp = arg;//隐式转换: c语言支持(GCC), C++不支持, 但我在测试时用G++编译同样通过
char *tmp1 = (char *)arg;//显式转换: c/c++都支持, 且符合标准要求, 使用这种方式为佳
//2.不能解引用
*arg = "other";//error
*tmp1 = "other";//正确, 操作变量时必须将void * 转换回原来的类型才可以解引用
//3.不能做指针算术
void * vptr = arg + 1;//error
char * cptr = tmp + 1;//可以, void *已经被转回char *
}
int main()
{
pthread_t cid;
char p[] = "hello world";
pthread_create(&cid, NULL, func, p);//func是线程的任务函数
}

    其次,void *的转换在C中是比较放松的,在C++中是比较严格的。比如在C中,支持void *隐式转换char *。(但实际上我在测试的时候C/C++隐式转换都支持,因此为了规范,大家还是显式转换为佳。)

    另外,void *类型转换要保证类型绝对原型转换,不能随意丢弃限定符

const char * p = "hello world";//被const限定为常量字符串
const void *vp = p;
const char *tmp = (const char *)vp;//一模一样转换回去

2. 函数指针不可以 void *转换

    函数指针不是对象指针,不可以转换成void *类型。

int add(int x) { return x + 1; }
void func(void) {
int (*pf)(int) = add;
void *pv = pf; //error, 标准不允许函数指针与 void* 互转
}

    想要将一个函数指针传进参数类型是void *的函数中,安全可移植的方式是:一、传指向函数指针的指针(这是最常用的方式)。二、可扩展式,即将函数指针用结构体包裹,除了能传函数指针外,还可以额外添加数据。

(1)传指向函数指针的指针
typedef int (*fn_t)(int);//另命名fn_t为一个函数指针,返回是int, 参数也是int
int add(int x) { return x + 1; }
void func(void *p)
{
fn_t  f = *(fn_t *)p;//将void * 还原为 fn_t
printf("%d\n", f(1));//调用
//扩展: 如果不用typedef另命名, 那么上述代码会很复杂,将会是下面这样:
int (*f)(int) = *(int (**)(int))p;//fn_t  f = *(fn_t *)p;
//是不是看得头皮发麻,这里就不展开讲函数指针了
}
int main()
{
fn_t f = add;
func(&f);//传 指向函数指针的指针
return 0;
}
(2)用结构体包裹
typedef int (*fn_t)(int);
struct payload
{
fn_t fn;
int age;//还可以额外携带数据
};
void func(void *arg)
{
struct payload *p = arg;//void * 转换成 payload *
printf("%d\n", p->fn(p->age));
}
int add(int x){ return x + 1; }
int main()
{
struct payload pf = {add, 20};
func(&pf);//传结构体指针,让结构体指针payload *隐式转换成 void *
return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/926146.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

常德网站优化公司东莞大岭山天气预报

【我是谁】 1.学历&#xff1a;22届双非本科校企合作&#xff08;软外&#xff0c;软件工程服务外包&#xff09;&#xff0c;编程课大部分是印度的NIIT老师上课&#xff0c;印式英语一点儿听不懂。。。所以大学全都自学的&#xff0c;和非科班的也没什么区别和优势&#xff0c…

PowerShell注意点

$()和${}的区别: $()表示命令替换,将括号内的命令执行后得到的输出作为值。 例如,$(ls)将会执行ls命令后得到当前目录下的文件列表作为值。 ${}表示变量替换,将大括号内的变量的值作为值。 例如,${a}将取变量a的值…

自动化脚本的自动化执行实践 - 详解

自动化脚本的自动化执行实践 - 详解2025-10-03 17:36 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !imp…

做商业网站的服务费维护费直播型网站开发

在Kotlin中&#xff0c;注解&#xff08;Annotations&#xff09;是一种用于在程序代码中添加元数据的特殊标记。它们提供了对代码的描述性信息&#xff0c;但本身并不会影响程序的运行。注解可以应用于类、方法、属性等程序元素上&#xff0c;用于提供关于这些元素的额外信息。…

m3u8转mp4软件中文版推荐与使用指南

近年来,随着在线视频的普及,m3u8格式的流媒体文件变得越来越常见。不少用户希望将m3u8文件转换为通用的mp4格式,便于本地保存、播放或分享。那么,选择一款好用的m3u8转mp4软件中文版,就成了很多小伙伴的需求。下面…

Unity简易事件分发器

一、EventFunctionusing System; namespace EventCore {public struct EventFunction{public object _caller;public Action _action;}public struct EventFunction<T>{public object _caller;public Action<…

react怎么做pc网站外贸soho建站

本文给大家整理了腾讯视频网页下载_腾讯视频怎么下载视频方面的内容。腾讯视频独播剧质量还是可以的&#xff0c;比较给力的是腾讯视频大量买入了老剧的版权&#xff0c;不乏一些比较经典的港剧&#xff0c;还把这些老剧修复了。腾讯视频播放器是一款支持多种音视频格式的主流播…

实用指南:1、docker入门简介

实用指南:1、docker入门简介pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco"…

调试parlant的大模型配置,最终自己动手写了g4f的模块挂载 - 教程

调试parlant的大模型配置,最终自己动手写了g4f的模块挂载 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "…

网站模板如何使用 如何修改吗网站视频插件

合并分支用rebase还是merge&#xff1f; 实际开发工作的时候&#xff0c;我们都是在自己的分支开发&#xff0c;然后将自己的分合并到主分支&#xff0c;那合并分支用2种操作&#xff0c;这2种操作有什么区别呢&#xff1f; git上新建一个项目&#xff0c;默认是有master分支…

迁安做网站教育培训机构设计图

Java核心类库篇6——IO 1、File 1.1、构造方法 方法声明功能介绍public File(File parent, String child)从父抽象路径名和子路径名字符串创建新的 File实例public File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例public File(String pa…

12380网站建设情况报告网站总体规划设计说明

hive分区重命名后&#xff0c;新的分区的分区大小为0 , 例如 alter table entersv.ods_t_test partition(dt2022-11-08) rename to partition(dt2022-11-21) ods_t_test 的2022-11-21分区大小为0。怎样修复 使用 msck repair table 命令来修复表的元数据&#xff0c;让hive重新…

太极 - MKT

太极 环境 下雨 下午 卧室 附上音乐 (沙石头 鱼儿 本身不也是物质的一部分么,都在不同的层次适应存在。 石头在河里打磨成圆滑,在沙漠变成啥子,这么看好像都是被动的过程。 但本质沙子石头都是原子层面的硅原子在…

佛山营销网站旅游网站建设方案后台

0-1背包理论基础 基础 DP数组与其下标的含义 dp[i][j]&#xff0c;i为物品编号&#xff0c;j为背包容量 dp[i][j]表示从下标为[0-i]的物品里任意取&#xff0c;放进容量为j的背包&#xff0c;价值总和最大是多少。 递推公式 分类&#xff1a;是否要放入下标为i的物品&…

网站建设人员职责分布昌吉网站建设咨询电话

一、智能家居与会议系统 智能家居与会议系统分论坛将于3月28日同期举办&#xff01; 智能会议系统它通过先进的技术手段&#xff0c;提高了会议效率&#xff0c;降低了沟通成本&#xff0c;提升了参会者的会议体验。对于现代企业、政府机构和学术界是不可或缺的。在这里&#x…

题解:P12410 「知りたくなかった、失うのなら」

草 -我ら不会と算に时む复なりlink 说在前面 如果你看了这个东西你最好就看个乐子别真的去写,卡常卡死你。 做法什么的请直接看正文。 注意到其他题解给出了很优美的做法,那么我就来点不优美的。 先设几个数字吧,设…

unity面向组合开发二:EC的代码实践

一、ECCore 需要在Unity项目中使用插件:UniRx,通过UniRx代替Mono的Update,Mono下做轮询性能消耗会有点大。 EntityMono代码:using System; using System.Collections.Generic; using EC; using UniRx; using Unity…

《咳咳,未来编程大师,顶尖程序员的第一条博客》

Helloooooo World!本人目前是一个在校大二的学生,正在备战蓝桥杯,希望有相同目标的朋友联系我,我们可以一起备赛,一起刷题。我的目标是在2026蓝桥杯比赛上拿下国一,哈哈哈哈虽然听起来很扯,但是我是会用拿国一的…

CSP-JF36

CSP-JF36T2 B. 最小的公倍数小题 ((10^L / 210) + 1) * 210 就是最小值#include <bits/stdc++.h> using namespace std;int n; int main(){// for(int i = 2; i <= 18; i++){ // long long x = pow…

airsim多无人机+无人车联合仿真辅导 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …