文章目录
- 一、字符指针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;
}