awfufu

返回

C指针和结构体Blur image

上一章

基础语法

C语言快速入门

指针类型#

什么是指针#

指针是 C 语言的精髓,指针也是一种变量,可以存储变量在内存中的地址。

  • &: 取地址运算符
  • *: 寻址运算符(解引用)
#include <stdio.h>

int main() {
  int var = 20;
  int* p = &var; // * 表示这是一个 int 类型的指针,将 var 的地址赋值给 p

  printf("&var: %p\n", &var); // %p 打印地址
  // &var: 0x7ffe56d638dc
  printf("p: %p\n", p);
  // p: 0x7ffe56d638dc
  printf("*p: %d\n", *p);
  // *p: 20

  return 0;
}
c

这里的指针变量 p 存储了 var 变量所在的内存块的地址。使用 *p 可以从这个地址访问 var,并获取 var 的值。

20 var (int) 0x7ffe56d638dc 0x7ffe56d638dc p (int*) 0x7ffe56d638e0

通过指针,我们可以直接访问和操作内存,这使得 C 语言非常强大且高效,但也需要小心使用以避免错误(如空指针、野指针)。

空指针和野指针#

字面意思,空指针就是指向空地址(地址为零)的指针;野指针就是指向无意义地址或非法地址的指针。

stddef.h 头文件中,定义了 NULL,它可以用来表示空指针。

#define NULL ((void *)0)
c
int* p1 = NULL; // 空指针:指向0
int* p2;        // 野指针:未初始化,内容随机
c

这种指针要小心使用,一旦对它们进行解引用操作,就会导致未定义行为。比如访问 *p1 时,程序会尝试访问零地址处的内存,这通常会导致程序崩溃。

#include <stdio.h>

int main() {
  int* p = NULL;
  printf("%d\n", *p); // segmentation fault
  return 0;
}
c

在访问指针之前,必须小心谨慎,确保指针被正确地初始化了且不为空。

#include <stdio.h>

int main() {
  int a = 10;
  int* p = &a;

  if (!p) {
    printf("p is null!\n");
    return 1;
  }

  printf("%d\n", *p);
  return 0;
}
c

指针的大小#

指针本身也是个变量,所以它也需要占用内存空间来存储。在 32 位(bit)系统上,指针的大小是 4 字节,在 64 位系统上则是 8 字节。这里的“位”指的是地址总线的位数,现代机器通常是 64 位。

在 64 位机器上,可以给 gcc 命令添加 -m32 参数来生成 32 位的可执行文件进行测试。

#include <stdio.h>

int main() {
  int* p = NULL;
  printf("sizeof(p): %zu\n", sizeof(p));
  return 0;
}
c

在 32 位系统上:

gcc main.c -m32 -o main
./main                 
# sizeof(p): 4
sh

在 64 位系统上:

gcc main.c -m64 -o main
./main                  
# sizeof(p): 8
sh

指针的作用#

在函数内部定义的变量称为局部变量。它们通常存储在栈(Stack)上,其生命周期仅限于函数执行期间。当函数执行完后,这些变量会被销毁,内存会被回收。

#include <stdio.h>

void foo(int x) {
  x = 100; // 修改数值
  printf("foo: x = %d\n", x);
}

int main() {
  int x = 10;
  foo(x);
  printf("main: x = %d\n", x);
  return 0;
}

// Output:
// foo: x = 100
// main: x = 10
c

因为 C 语言是按值传递(Pass by Value)的,直接传递变量只会拷贝一份副本。如果想在函数内部修改函数外部定义的变量,则需要传递变量的地址(指针),这样函数就可以通过地址直接操作原始数据。

main.c
#include <stdio.h>

// 交换两个整数的值 使用指针
void swap(int* a, int* b) {
  int temp = *a;
  *a = *b;
  *b = temp;
}

int main() {
  int x = 5, y = 10;

  printf("Before: x = %d, y = %d\n", x, y);
  swap(&x, &y); // 传递地址
  printf("After:  x = %d, y = %d\n", x, y);

  return 0;
}
c

数组和指针#

数组名即指针#

数组名在大多数表达式中会“退化”为指向数组第一个元素的指针。

int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // = &arr[0]

printf("p:       %p\n", p);
printf("arr:     %p\n", arr);
printf("&arr[0]: %p\n", &arr[0]);

// Output:
// p:       0x7ffe56d638dc
// arr:     0x7ffe56d638dc
// &arr[0]: 0x7ffe56d638dc
c

在上面的示例中,p 的值等于 arr 的值等于 &arr[0] 的值。

printf("arr[0] = %d\n", arr[0]);
printf("*p     = %d\n", *p);

// Output:
// arr[0] = 10
// *p     = 10
c

arr[0]*p 都可以访问数组的第一个元素。

指针的运算#

指针可以进行加减运算,加法运算会将指针移动到下一个元素的位置,减法运算会将指针移动到前一个元素的位置。

printf("arr[1]   = %d\n", arr[1]);
printf("*(p + 1) = %d\n", *(p + 1));
printf("p[1]     = %d\n", p[1]);

// Output:
// arr[1]   = 20
// *(p + 1) = 20
// p[1]     = 20
c

对 int 类型的指针 p 执行 p + 1 会将指针移动到下一个 int 的位置。指针 p 也可以当作一个 int 数组进行下标访问,这里的 p[1] 等价于 *(p + 1)

你可以尝试不同类型的指针,比如 char*,你会发现指针移动的步长是 1。

char arr[] = {'a', 'b', 'c', 'd', 'e'};
char* p = arr;

printf("p: %p\n", p);
printf("p + 1: %p\n", p + 1);
printf("p - 1: %p\n", p - 1);
c

字符串#

之前说过,字符串实际上就是字符数组,并且以空字符 \0 结尾。

char str[] = "hello"; 
char* p = str;

printf("str: %s, p: %s\n", str, p);
// str: hello, p: hello

p[0] = 'H';
printf("str: %s, p: %s\n", str, p);
// str: Hello, p: Hello
c

操作字符串#

C 标准库提供了一些用于操作字符串的函数,这些函数都定义在 string.h 头文件中。

#include <stdio.h>
#include <string.h>

int main() {
  char str1[10] = "hello";
  char str2[10];

  printf("strlen(str1): %zu\n", strlen(str1));
  strcpy(str2, str1);
  printf("str2: %s\n", str2);

  return 0;
}
c

使用指针,我们可以高效地实现 strcpy 函数。

char* strcpy(char* dest, const char* src) {
  char* p = dest;
  while (*dest++ = *src++);
  return p;
}
plaintext

你可以通过 string.h 文档 学习更多接口。

函数指针#

函数名即指针#

就像数组名是数组第一个元素地址的指针一样,函数名也是指向函数地址的指针。我们可以定义一个指针变量来存储函数的地址,这就是函数指针。

#include <stdio.h>

void say_hello() {
  printf("Hello World!\n");
}

int add(int a, int b) {
  return a + b;
}

int main() {
  // 定义指向 say_hello 的指针
  void (*func_ptr)();
  func_ptr = say_hello;
  
  // 通过指针调用函数
  func_ptr(); // 或 (*func_ptr)();

  // 定义指向 add 的指针
  int (*add_ptr)(int, int) = add;
  printf("2 + 3 = %d\n", add_ptr(2, 3));
  return 0;
}
c

回调函数#

不像其他高级语言,C 语言没有类似 lambda 表达式或闭包的语法糖,但是我们可以通过函数指针来实现类似的功能。

函数指针的一个主要用途是实现回调函数(Callback Function)。回调函数是指将一个函数指针作为参数传递给另一个函数,由另一个函数在适当的时候调用这个函数。

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

// 接受函数指针作为参数
void compute(int (*op)(int, int), int a, int b) {
  int result = op(a, b);
  printf("Result: %d\n", result);
}

int main() {
  compute(add, 10, 5); // Result: 15
  compute(sub, 10, 5); // Result: 5
  return 0;
}
c

C 语言标准库中的 qsort 就是一个典型的例子,它允许用户自定义比较逻辑。

#include <stdio.h>
#include <stdlib.h>

int compare(const void* a, const void* b) {
  return (*(int*)a - *(int*)b);
}

int main() {
  int arr[] = {5, 2, 9, 1, 5, 6};
  int size = sizeof(arr) / sizeof(arr[0]);

  qsort(arr, size, sizeof(int), compare);

  for (int i = 0; i < size; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
  
  return 0;
}
c

结构体#

结构体类型#

结构体(Structure)是 C 语言中一种用户自定义的数据类型,允许将不同类型的数据组合成一个单一的类型。

#include <stdio.h>
#include <string.h>

// 定义一个结构体类型
struct Person {
  char   name[50];
  int    age;
  double height;
};

int main() {
  // 声明一个结构体变量
  struct Person person;

  // 访问和修改成员并赋值
  strcpy(person.name, "Alice"); // 注意:字符串赋值不能直接使用 =,需要使用 strcpy
  person.age = 20;
  person.height = 1.65;

  printf("Name: %s, Age: %d, Height: %.2f\n", person.name, person.age, person.height);
  return 0;
}
c

结构体指针#

当我们将结构体的地址传递给指针时,如果我们想通过指针访问结构体的成员,可以使用 -> 运算符。

通常情况下,解引用指针并访问成员的语法是 (*p).member,但这写起来比较麻烦(因为 . 的优先级高于 *,必须加括号)。C 语言提供了一个简写方式:p->member

#include <stdio.h>
#include <string.h>

struct Person {
  char   name[50];
  int    age;
};

int main() {
  struct Person person;
  struct Person* p = &person;

  // 使用指针访问成员
  strcpy(p->name, "Bob"); // 等价于 (*p).name
  p->age = 30;            // 等价于 (*p).age

  printf("Name: %s, Age: %d\n", p->name, p->age);
  return 0;
}
c

结构体的大小#

结构体的大小并不总是其所有成员大小之和,因为编译器为了提高 CPU 访问内存的效率,会对结构体成员进行“内存对齐”(Memory Alignment)。

对齐规则:每个成员的偏移量(Offset)必须是该成员大小的整数倍(或者是编译器默认对齐数的较小值)。结构体的总大小必须是其内部最大成员(基本类型)大小的整数倍。如果不足,会在末尾填充字节。

让我们详细分析一下 struct Pack 的内存布局:

struct Pack {
  char c;   // 1 byte
  int i;    // 4 bytes
};
c
  • 成员 c (char) 占用 1 字节。放置在偏移量 0 处。
  • 成员 i (int) 占用 4 字节。根据对齐规则,它的起始地址必须是 4 的倍数。当前的偏移量是 1(c 之后),不是 4 的倍数。
  • 编译器会在 c 后面插入 3 个字节的填充(Padding),使偏移量变为 4。
  • i 从偏移量 4 开始存储,占用第 4-7 字节。
  • 此时结构体总大小为 8 字节。8 是最大成员大小 (4) 的整数倍,符合整体对齐规则。

最终的内存布局:

c 0 pad 1 pad 2 pad 3 i 4 i 5 i 6 i 7 char (1B) int (4B)
#include <stdio.h>

int main() {
  printf("sizeof(struct Pack) = %zu\n", sizeof(struct Pack));
  // Output: sizeof(struct Pack) = 8
  return 0;
}
c

因此,在计算结构体大小时,务必使用 sizeof 运算符,而不要简单地将成员大小相加。有时通过调整成员的定义顺序(例如将所有 char 放在一起),可以减少填充字节,从而减小结构体的大小。

自定义类型#

在 C 语言中,可以使用 typedef 关键字来定义自定义类型。

main.c
#include <stdio.h>

typedef int MyInt;

int main() {
  MyInt a = 10;
  printf("a = %d\n", a);
  return 0;
}
c

同样的,可以把一个复杂类型定义为自定义类型。

main.c
#include <stdio.h>

typedef struct Point {
  int x;
  int y;
} Point;

int main() {
  Point p;
  p.x = 10;
  p.y = 20;
  printf("p.x = %d, p.y = %d\n", p.x, p.y);
  return 0;
}
c
C指针和结构体
https://awfufu.com/blog/c-pointers-and-structs
Author awfufu
Published at 2022年6月11日
Comment seems to stuck. Try to refresh?✨