指针是内存的通行证
代码是指导计算机执行任务的指令,数据是指导计算机执行任务的原料,而指针就是指导计算机如何找到数据的通行证。修改数据除了直接修改数据本身,还可以通过指针来修改。
int data = 0;
int *p = &data; // p指向data
*p = 1; // 将data的值修改为1
在内存的眼里,一切都是01比特的组合。内存中没有类型。也恰恰因为这一点,指针才能指向任何类型的数据,因为指针本身没有类型,它只是一个地址而已。
解引用
*
是解引用符号,可以访问指针指向的数据。与之相对的&
是取地址符,可以访问数据对应的地址。
int data = 0;
int *p = &data;
printf("%d\n", *p); // 0
值拷贝和引用
通俗的讲,值拷贝是将源数据拷贝一份,引用则是将源数据对应的地址拷贝一份。前者不会影响源数据,后者会影响源数据。从内存分配的角度来讲,值拷贝会将源数据拷贝一份放到栈上,而引用则是将源数据对应的地址拷贝一份放到栈上。
void modify(int data){
data = 1;
}
int data = 0;
modify(data);
printf("%d\n", data); // 0
void modify_ref(int* data){
*data = 1;
}
modify_ref(&data);
printf("%d\n", data); // 1
特殊的,当传递给函数的参数是数组时,数组会退化为指针,这时候就是引用了。(数组名是数组的首地址)
void modify(int* data){
data[0] = 1;
}
int data[1] = {0};
modify(data);
printf("%d\n", data[0]); // 1
函数返回值
函数返回值的本质是将返回值拷贝一份放到栈上,所以返回值不能是数组,因为数组是不能拷贝的。但是可以返回指针,因为指针是可以拷贝的,也可以返回一个具有固定大小的值,比如结构体或者基础类型。
一个常见的错误是悬垂引用,即返回了一个指向栈上数据的指针,这样的指针在函数返回后就会失效。
int* get_data(){
int data = 0; // data在栈上
return &data; // 返回data的地址
}
int* p = get_data();
printf("%d\n", *p); // 报错,因为函数已经返回了,data已经被销毁了
一个返回结构体的例子
typedef struct{
int a;
int b;
} data;
data get_data(){
data d = {0, 1};
return d;
}
data d = get_data();
printf("%d,%d\n", d.a, d.b); // 0, 1
指针+1
指针+1并不是指针的值加1,而是指针指向的地址加上1。因为指针指向的是一个数据,而数据的地址是连续的,所以指针+1就是指向下一个数据的地址。之所以可以这样做,是因为数组在内存中是连续存在的。
int data[2] = {0, 1};
int *p = data;
*(p + 0) = 100;
*(p + 1) = 101;
printf("%d,%d\n", data[0], data[1]); // 100, 101
转换
由于数据在内存中的连续性,因此可以将任何连续的内存转换成对应的连续的逻辑结构,比如下面的例子,就将含有三个int的结构体转换成了一个int数组。
typedef struct{
int a;
int b;
int c;
} data;
data d = {0, 1, 2};
// &d 是结构体的地址
// (int*)&d 是将该结构体地址对应的数据转换成int*类型,即整型数组
int *p = (int *)&d;
for(int i = 0; i < 3; i++){
printf("%d\n", p[i]);
}
相同的,也可以将含有两个32位整型的结构体转换为一个64位整型。由于结构体与整型无法直接转化,所以可以直接将结构体指针指向的数据转换为整型指针指向的数据。
typedef struct{
int offset;
int id;
} data;
data d = {0, 1};
uint64_t data = *(uint64_t*)&d;
使用64位整型隐藏数据
有些场景下,在创建了结构体之后,不希望用户直接对结构体进行操作,而是必须强制用户使用对应的接口进行操作,那么就可以通过将指针转换成64位整型来隐藏数据,这样用户就无法直接对结构体进行操作了。
// 真正存放数据的结构体
typedef struct {
char* username;
char* address;
int age;
} _user_real;
// 存放隐藏地址的盒子,暴漏给用户的结构体
typedef struct {
uint64_t __placeholder;
} user;
通过user
来保存真正的数据,而_user_real
则是真正存放数据的结构体。这样用户就无法直接对_user_real
进行操作了,只能通过user
来操作。
// 创建一个用户
user
createUser()
{
_user_real* u = malloc(sizeof(_user_real));
user u1 = { .__placeholder = (uint64_t)u };
return u1;
}
// 设置用户名
void
set_username(user u, char* username)
{
_user_real* u1 = (void*)u.__placeholder;
u1->username = username;
}
// 获取用户名
char*
get_username(user u)
{
_user_real* u1 = (void*)u.__placeholder;
return u1->username;
}
// 释放用户
void
freeUser(user u)
{
_user_real* u1 = (void*)u.__placeholder;
free(u1);
}
int
main()
{
user u = createUser();
set_username(u, "hello");
printf("%s\n", get_username(u)); // hello
freeUser(u);
return 0;
}
sds字符串
sds字符串是redis中实现的高性能字符串结构,其结构体通过一个十分巧妙的设计,从而使得sds结构体可以使用标准库里面的字符串处理函数。
typedef char* sds;
struct sdshdr {
int free; // 长度
int len; // 剩余空间
char buf[]; // 柔性数组
};
// 创建一个sds字符串
sds
createSds(char* str)
{
int len = strlen(str);
struct sdshdr* s = malloc(sizeof(struct sdshdr) + len + 1);
s->free = 0;
s->len = len;
memcpy(s->buf, str, len + 1);
return s->buf;
}
// 根据s->buff获取到sds结构体的地址,即字符串地址减去结构体的大小
#define SDS_HDR(s) ((struct sdshdr*)((s) - (sizeof(struct sdshdr))))
其巧妙之处在于,结构体内最后一个成员是一个柔性数组,在内存布局上,是连续存在的。可以通过s->buff
的地址反推得到sdshdr
对应的地址。这样既可以使用标准库里面的字符串处理函数,又可以通过s->buff
的地址反推得到sdshdr
对应的地址,从而可以获取到s->free
和s->len
的值。
| free | len | buf .... |
0 4 8
8 - 8 = 0(结构体头地址)