目录
指针
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.二级指针
二级指针变量是用来存放一级指针变量的地址的
-
定义:
int a = 10;
:声明一个整数变量a
并赋值为 10。int *pa = &a;
:声明一个一级指针pa
,指向变量a
的地址。int **ppa = &pa;
:声明一个二级指针ppa
,指向一级指针pa
的地址。
-
指针关系:
pa
存储a
的地址。ppa
存储pa
的地址。
-
访问变量:
- 通过
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
, b
和 c
。
访问元素
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.结构体内存对齐
结构体的对齐规则
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值
o VS中默认的值为8 - 结构体总大小为
最大对齐数
(每个成员变量都有一个对齐数)的整数倍。 - 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么存在内存对齐?
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的方法。(让占用空间小的成员尽量集中在一起)
可以使用#pragma pack来设置整个编译单元的对齐方式
动态内存管理
1.动态内存函数的介绍 ★
1.1 malloc
和free
函数:
malloc
函数用于在堆上动态分配一块指定大小的内存,并返回指向该内存块的指针。返回的指针类型为 void*
,通常需要强制类型转换为适当的指针类型。
关键点:
- 函数签名:
void* malloc(size_t size);
- 功能:分配
size
字节的未初始化内存,并返回指向该内存块起始位置的指针。 - 返回值:若分配成功,返回指向分配内存的指针;若失败,返回
NULL
。 - 注意事项:
- 分配的内存未初始化,可能包含不确定的值。
- 使用完毕后需使用
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*
,通常需要强制类型转换为适当的指针类型。
关键点:
- 函数签名:
void* calloc(size_t num, size_t size);
- 功能:分配可以存储
num
个元素的内存,每个元素大小为size
字节,并将所有分配的内存初始化为零。 - 返回值:若分配成功,返回指向分配内存的指针;若失败,返回
NULL
。 - 注意事项:
- 分配的内存已被初始化为零。
- 使用完毕后需使用
free
释放内存,以避免内存泄漏。 - 如果
num
或size
为 0,返回的指针依赖于具体实现,但不应解引用。
示例代码:
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL) {
printf("Memory allocation failed: %s\n", strerror(errno));
return 1;
}
calloc
与 malloc
的区别:
- 初始化:
malloc
分配的内存未初始化,可能包含不确定的值;calloc
分配的内存会被初始化为 0。 - 参数:
malloc
只接收一个参数(要分配的字节数),而calloc
接收两个参数(元素数量和每个元素的字节数)
1.3 realloc
realloc
函数用于重新分配内存块的大小,并将数据尽可能保留到新分配的内存块中。它可以增加或减少已经分配的内存,并返回指向新内存块的指针。
关键点:
- 函数签名:
void* realloc(void* ptr, size_t size);
- 功能:调整由
ptr
指向的内存块的大小为size
字节,并返回新内存块的指针。 - 返回值:
- 若成功,返回指向新分配内存的指针。
- 若失败,返回
NULL
,原来的内存块保持不变(仍然有效)。
- 行为:
- 如果
ptr
为NULL
,realloc
等同于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
可用于动态调整数据结构的大小,而无需手动管理数据复制和内存释放。