指针与内存

关注微信公众号塔容万物

指针是内存的通行证

代码是指导计算机执行任务的指令,数据是指导计算机执行任务的原料,而指针就是指导计算机如何找到数据的通行证。修改数据除了直接修改数据本身,还可以通过指针来修改。

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->frees->len的值。

| free | len | buf .... |
0      4     8

8 - 8 = 0(结构体头地址)