C语言指针/结构体/动态内存管理-数据结构先修课

指针

1.什么是指针

1. 指针:是一个内存地址,用于指向存储在计算机内存中的某个数据
2. 指针变量:是一个变量,用于存储这个内存地址,通常我们说指针意思是指针变量

一句话总结指针是一个内存地址,而指针变量是存储该地址的变量

指针的大小
32位系统中,指针通常占用 4个字节
64位系统中,指针通常占用 8个字节

int var = 10;      
int *ptr = &var;   // 定义一个指针变量,并将其初始化为变量 var 的地址

// 输出变量 var 的值
printf("%d\n", var);

// 输出变量 var 的地址(即指针)
printf("%p\n", &var);

// 输出指针变量 ptr 的值(即变量 var 的地址)
printf("%p\n", ptr);

// 使用指针变量 ptr 间接访问变量 var 的值
printf("%d\n", *ptr);

2.指针类型

在 C 语言中,指针类型定义如下:

int *int_ptr;      // 指向整型的指针,解引用访问4个字节
char *char_ptr;    // 指向字符型的指针,解引用访问1个字节
float *float_ptr;  // 指向浮点型的指针
double *double_ptr;// 指向双精度浮点型的指针

指针类型的意义
● 数据访问与解引用:指针类型决定了在解引用指针时,从内存中读取的数据类型

int var = 10;
int *int_ptr = &var;
printf("%d\n", *int_ptr); // 输出 10

● 指针算术运算:指针类型还影响指针算术运算的行为

int arr[5] = {1, 2, 3, 4, 5};
int *int_ptr = arr;
printf("%p\n", int_ptr);
int_ptr++;
printf("%p\n", int_ptr); // 增加 4 个字节

3.野指针

是指在程序中指向一个已经被释放或不再有效的内存位置的指针

野指针的成因
1.使用未初始化的指针

int* ptr; // 未初始化,ptr 指向未知地址
*ptr = 5; // 未定义行为

2.释放内存后未置空指针

int* ptr = (int*)malloc(sizeof(int));
free(ptr);
// ptr 仍然指向原内存地址,但该内存已经被释放

3.超出变量作用域

int* createPointer() {
    int a = 10;
    return &a; // 返回局部变量的地址,超出函数作用域后该地址不再有效
}
int* ptr = createPointer();

如何规避野指针

1.在声明指针时进行初始化
2.注意指针操作的边界,避免越界访问
3.在释放内存后将指针置为 NULL
4.避免返回局部变量的地址
5.在使用指针之前,检查其是否为 NULL

4.指针运算

指针减法:
两个指针相减的结果是它们之间相隔的元素个数(而不是字节数)。这只在两个指针指向同一个数组的元素时才有意义。

int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[4];  // p1指向arr[4]
int *p2 = &arr[1];  // p2指向arr[1]

int distance = p1 - p2;  // distance为3,因为它们之间有三个元素

指针与指针的比较:
可以比较两个指针是否指向同一个地址,或者一个指针是否在另一个指针之前或之后

if (p == &arr[2]) {
    // 如果p指向arr[2],则执行此代码
}

if (p > &arr[0]) {
    // 如果p指向的地址在arr[0]之后,则执行此代码
}

5.指针和数组

1.数组名作为指针:
arr 实际上是数组首元素的地址,相当于 &arr[0]
可以通过 int *p = arr将指针 p 指向数组的第一个元素。
2.指针算术:
p + i 将指针向前移动 i 个元素的位置
&arr[i]p + i 的结果相同,因为数组元素在内存中是连续存储的。
3.访问数组元素:
可以通过数组下标 arr[i] 访问。
可以通过指针算术 *(p + i) 访问。
可以通过数组名加偏移量 *(arr + i) (完全等价于arr[i])访问。

#include <stdio.h>
int main() {
	int arr[10] = {0};          
	int *p = arr;               
	int sz = sizeof(arr) / sizeof(arr[0]);  
	int i = 0;
	printf("%d ", arr[i]);
	printf("%d ", *(p + i));
	printf("%d\n", *(arr + i));
	
	for (i = 0; i < sz; i++) {
		printf("%p ----- %p\n", &arr[i], p + i);  
	}
}

6.二级指针

二级指针变量是用来存放一级指针变量的地址的

  1. 定义

    • int a = 10;:声明一个整数变量 a 并赋值为 10。
    • int *pa = &a;:声明一个一级指针 pa,指向变量 a 的地址。
    • int **ppa = &pa;:声明一个二级指针 ppa,指向一级指针 pa 的地址。
  2. 指针关系

    • pa 存储 a 的地址。
    • ppa 存储 pa 的地址。
  3. 访问变量

    • 通过 pa 访问 a*pa
    • 通过 ppa 访问 pa 并间接访问 a**ppa
  • a 的地址为 0x0012ff40,值为 10。
  • pa 的值为 0x0012ff40,即 a 的地址。
  • ppa 的值为 0x0018ff32,即 pa 的地址。
#include <stdio.h>

int main() {
    int a = 10;        // 声明一个整数变量 a
    int *pa = &a;      // 声明一个指针 pa,指向 a
    int **ppa = &pa;   // 声明一个二级指针 ppa,指向 pa

    // 通过指针访问和修改 a 的值
    *pa = 20;
    printf("%d\n", a); // 输出 20

    return 0;
}

7.字符指针

字符指针(char *)是一种指针类型,它指向的是字符类型的数据

#include<stdio.h>

int main() {
    const char* p1 = "abcdef"; // 把字符串首字符 'a' 的地址赋值给 p1
    const char* p2 = "abcdef"; // 常量字符串无需创建,直接赋值
    char arr1[] = "abcdef";    // 创建一个字符数组 arr1,并初始化为 "abcdef"
    char arr2[] = "abcdef";    // 创建另一个字符数组 arr2,并初始化为 "abcdef"
    
    if (p1 == p2) 
        printf("p1==p2\n");
    else 
        printf("p1!=p2\n");
    
    if (arr1 == arr2) 
        printf("arr1==arr2\n");
    else 
        printf("arr1!=arr2\n");

    return 0;
}

总结

  • 常量字符串(通过指针指向的字符串)在编译器优化下通常只会存储一次,所有指向相同字符串的指针会指向同一个内存地址。
  • 字符数组是独立的内存区域,每个数组都有自己的地址,哪怕它们存储的是相同的字符串内容。
  • 比较指针时,比较的是它们指向的内存地址;比较数组时,也是比较它们的内存地址,而不是内容。

8.指针数组

指针数组是一个数组,其中每个元素都是一个指向特定数据类型的指针。它可以用来存储多个地址,从而实现对这些数据的灵活访问和操作。

定义

type *arrayName[size];
  • type 是指针指向的数据类型。
  • arrayName 是指针数组的名称。
  • size 是数组的大小。

示例

int a = 10, b = 20, c = 30;
int *parr[3] = {&a, &b, &c};

在这个例子中,parr 是一个指针数组,其中包含三个指针,分别指向变量 a, bc

访问元素

printf("%d\n", *parr[0]); // 输出 10
printf("%d\n", *parr[1]); // 输出 20
printf("%d\n", *parr[2]); // 输出 30

用途

  • 管理多个变量
    可以用来存储多个变量的地址,便于统一管理和操作。

  • 处理二维数组
    可以用指针数组来模拟二维数组,从而实现对二维数组的灵活操作。

    int arr1[4] = {1, 2, 3, 4};
    int arr2[4] = {5, 6, 7, 8};
    int arr3[4] = {9, 10, 11, 12};
    int *parr[3] = {arr1, arr2, arr3};
    
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", parr[i][j]);//也可写成是*(parr[i]+j)
        }
        printf("\n");
    }
    

9.数组指针

数组指针就是指向数组的指针
注意:数组名通常表示的都是数组首元素的地址。但是有两个例外:1.sideof (数组名),这里的数组名表示整个数组计算的是整个数组的大小,单位是字节;2.&数组名,这里的数组名表示的依然是整个数组,所以&数组名取出的是整个数组的地址。

  • 整形指针是用来存放整型的地址
  • 字符指针是用来存放字符的地址
  • 数组指针是用来存放数组的地址
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};  // 定义一个包含5个整数的数组
    int (*ptr)[5];  // 声明一个指向含有5个整数的数组的指针

    ptr = &arr;  // 将数组指针指向arr数组

    // 使用数组指针访问数组元素
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, (*ptr)[i]);  // (*ptr)相当于数组名
    }

    // 另一种方式使用数组指针
    int *p = (int *)ptr;  // 将数组指针转换为指向整型的指针
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, *(p + i));  // 通过普通指针访问数组元素
    }

    return 0;
}

10.数组参数和指针参数

一维数组传参

#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}

二维数组传参

void test(int arr[3][5])
{}

void test(int arr[][5])
{}


void test(int (*arr)[5])
{}

int main()
{
int arr[3][5] = {0};
test(arr);//二维数组的数组名表示首元素的地址(即第一行的地址)
}

一级指针传参

#include <stdio.h>
void print(int *p, int sz) {
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d\n", *(p + i));
    }
}
int main() {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int *p = arr;
    int sz = sizeof(arr)/sizeof(arr[0]); // 计算数组大小
    // 一级指针p,传给函数
    print(p, sz);
    return 0;
}

二级指针传参

#include <stdio.h>
void test(int** ptr)
{
    printf("num = %d\n", **ptr);
}
int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
}

反过来想,如果函数的形式参数是二级指针,调用函数的时候可以传什么实参

test(int** p)
{}
int* p1;
int** p2;
int* arr[10]; // 指针数组

test(&p1);
test(p2);
test(arr);

11.函数指针

把函数指针跟数组指针类比:指向函数的指针就是函数指针,指向数组的指针就是数组指针

#include <stdio.h>
int Add(int x, int y) {
    return x + y;
}

int main() {
    int arr[5] = { 0 };
    //&数组名 取出数组的地址
    int(*p)[5] = &arr;//数组指针
    //&函数名 取出函数的地址(&可不写)
    printf("%p\n", &Add);
    printf("%p\n", Add);

    int(*pf)(int,int)=&Add;
    //通过pf找到函数
    int result = (*pf)(2, 3);//*也可不写
    printf("%d\n", result);
    return 0;
}

函数指针的用途示例

#include <stdio.h>

// 定义加法函数
int add(int a, int b) {
    return a + b;
}

// 定义减法函数
int subtract(int a, int b) {
    return a - b;
}

// 定义乘法函数
int multiply(int a, int b) {
    return a * b;
}

// 定义除法函数
int divide(int a, int b) {
    if (b != 0)
        return a / b;
    else {
        printf("除数不能为0!\n");
        return 0;
    }
}

// 计算函数,使用函数指针
void calc(int (*pf)(int, int)) {
    int x = 0;
    int y = 0;
    int ret = 0;

    printf("请输入2个操作数: ");
    scanf("%d %d", &x, &y);

    ret = pf(x, y);
    printf("结果: %d\n", ret);
}

void menu() {
    printf("请选择运算:\n");
    printf("1. 加法\n");
    printf("2. 减法\n");
    printf("3. 乘法\n");
    printf("4. 除法\n");
    printf("5. 退出\n");
}

int main() {
    int input = 0;
    do {
        menu();
        printf("请选择: ");
        scanf("%d", &input);

        switch (input) {
            case 1:
                calc(add);
                break;
            case 2:
                calc(subtract);
                break;
            case 3:
                calc(multiply);
                break;
            case 4:
                calc(divide);
                break;
            case 5:
                printf("退出程序\n");
                break;
            default:
                printf("无效选择,请重试\n");
        }
    } while (input != 5);

    return 0;
}

结构体

1.结构体的声明和初始化

1.1结构体的基础知识
结构体是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量
使用结构体,是为了描述复杂对象。
1.2.结构体的声明、初始化及成员访问

struct tag
{
    member-list;
}variable-list;

示例代码

struct peo {//类型
	char name[20];
	char tele[12];
	char sex[10];
	int high;
}p1,p2;//变量列表可有可无,p1,p2是全局变量
struct stu {
	struct peo p;
	int num;
};
void print1(struct peo p) {
	printf("%s %s %s %d\n", p.name, p.tele, p.sex, p.high);//结构体变量.成员变量
}
void print2(struct peo*sp) {
	printf("%s %s %s %d\n", sp->name, sp->tele, sp->sex, sp->high);//结构体指针->成员变量
}
int main() {
	struct peo p3 = {"Jack","19876438753","Male",180};//结构体变量的创建,并初始化
	struct stu s1 = { {"Lucy","17654378655","female",170},23};
	printf("%s %s %s %d\n", p3.name, p3.tele, p3.sex, p3.high);
    printf("%s %s %s %d %d\n", s1.p.name, s1.p.tele, s1.p.sex, s1.p.high, s1.num);
    print1(p3);
    print2(&p3);
	return 0;
}

2.结构体传参

上述两个代码,首选print2函数进行传参。
原因:函数传参的时候,参数是需要压栈的,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大。会导致性能的下降
结论:
结构体传参的时候要传结构体的地址

3.结构的自引用

在数据结构中,自引用结构是指结构体内包含指向同类型结构体的指针。自引用结构通常用于表示链表、树等数据结构中的节点。

// 定义链表节点结构体
struct Node {
    int data;  // 节点中的数据
    struct Node* next;  // 指向下一个节点的指针
};

4.结构体内存对齐

结构体的对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处。
    对齐数=编译器默认的一个对齐数与该成员大小的较小值
    o VS中默认的值为8
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐?
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的方法。(让占用空间小的成员尽量集中在一起)
可以使用#pragma pack来设置整个编译单元的对齐方式

动态内存管理

1.动态内存函数的介绍 ★

1.1 mallocfree函数

malloc 函数用于在堆上动态分配一块指定大小的内存,并返回指向该内存块的指针。返回的指针类型为 void*,通常需要强制类型转换为适当的指针类型。

关键点

  1. 函数签名void* malloc(size_t size);
  2. 功能:分配 size 字节的未初始化内存,并返回指向该内存块起始位置的指针。
  3. 返回值:若分配成功,返回指向分配内存的指针;若失败,返回 NULL
  4. 注意事项
    • 分配的内存未初始化,可能包含不确定的值。
    • 使用完毕后需使用 free 释放内存,以避免内存泄漏。
    • 如果 size 为 0,返回的指针依赖于具体实现,但不应解引用。

示例代码

int* p = (int*)malloc(40);
if (p == NULL) {
    printf("Memory allocation failed: %s\n", strerror(errno));
    return 1;
}
free(p);
p=NULL;

1.2calloc函数

calloc 函数用于在堆上动态分配一块指定数量元素的内存,并将每个字节初始化为零。返回的指针类型为 void*,通常需要强制类型转换为适当的指针类型。

关键点

  1. 函数签名void* calloc(size_t num, size_t size);
  2. 功能:分配可以存储 num 个元素的内存,每个元素大小为 size 字节,并将所有分配的内存初始化为零。
  3. 返回值:若分配成功,返回指向分配内存的指针;若失败,返回 NULL
  4. 注意事项
    • 分配的内存已被初始化为零。
    • 使用完毕后需使用 free 释放内存,以避免内存泄漏。
    • 如果 numsize 为 0,返回的指针依赖于具体实现,但不应解引用。

示例代码

int* p = (int*)calloc(10, sizeof(int));
if (p == NULL) {
    printf("Memory allocation failed: %s\n", strerror(errno));
    return 1;
}

callocmalloc 的区别:

  • 初始化malloc 分配的内存未初始化,可能包含不确定的值;calloc 分配的内存会被初始化为 0。
  • 参数malloc 只接收一个参数(要分配的字节数),而 calloc 接收两个参数(元素数量和每个元素的字节数)

1.3 realloc

realloc 函数用于重新分配内存块的大小,并将数据尽可能保留到新分配的内存块中。它可以增加或减少已经分配的内存,并返回指向新内存块的指针。

关键点

  1. 函数签名void* realloc(void* ptr, size_t size);
  2. 功能:调整由 ptr 指向的内存块的大小为 size 字节,并返回新内存块的指针。
  3. 返回值
    • 若成功,返回指向新分配内存的指针。
    • 若失败,返回 NULL,原来的内存块保持不变(仍然有效)。
  4. 行为
    • 如果 ptrNULLrealloc 等同于 malloc,分配新的内存。
    • 如果 size 为 0,realloc 的行为等同于 free,释放内存并返回 NULL
    • 如果新大小比原来大,未初始化的部分内容为不确定值。

示例代码

int* p = (int*)realloc(p, 80);
if (p == NULL) {
    printf("Memory reallocation failed: %s\n", strerror(errno));
    return 1;
}

malloc 的关系:

  • realloc 允许调整现有内存块的大小,而 malloc 只负责分配新的内存块。
  • realloc 可用于动态调整数据结构的大小,而无需手动管理数据复制和内存释放。
  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值