C语法Day4-指针
# C指针
指针是初学者比较难理解的一个概念,很容易绕晕。
在理解指针之前,学者需要先了解 数据存储和内存分配的一些基本概念,
存储介质:我们数据都是存储在硬盘和内存条等物理介质上的,这好比图书馆的书架
数据:数据一块一块的存储好比是书架上的书
定义一个整形变量:
int a = 5; // 定义一个整形变量,系统将分配一块内存给变量a。而内存中存储数据5
定义一个变量a 好比在书架某个位置上放置了一本书,而数据在内存中的地址好比书架号
通常我们根据书架号在【哪个区,哪一排,哪一列】来找到书本的,而找书通常是由【人】来找。
那么在内存中又是如何找到对应的数据呢?
# 内存地址&
**&a:**获取变量a的内存地址
int a = 5;
printf("a的内存地址:%p ", &a);
// 打印结果:a的内存地址:0x7ffde30e7ba4
在变量a前添加&,即能获取变量a在内存中的内存地址0x7ffde30e7ba4 (相当于书架的编号),这个内存地址对应的好比书架的某一个盒子,而更换这个【盒子】上的书本,等同于直接修改了变量a的值
现实中图书馆是【人】走动来找到书的,那么内存条中找书架的又是谁呢?
# 转译内存地址值 *
*(&a): 通过【 “指针” 】星号【 * 】来获取内存地址对用的数据值
看一段代码:
int a = 5;
printf("a的内存地址:%p ", &a); // 打印a的地址 0x7ffde30e7ba4
printf("a = %d", *(&a)); // 打印a的值5
a 变量;
&a 指 a的内存地址;
*(&a) 指向 a的内存地址上的值 = a 的值,那么可以理解为: a = *(&a)
# 【 * p 】 指针变量
⚠️ 当声明一个变量并且在变量名前使用 【*】修饰的时候,那么这个变量就表示存储对应数据类型的内存地址的对象,比如
int a = 5;
float b = 5.123;
int *p; // 定义指针变量p
// int 声明的指针变量p 只能存储 int 类型数据的内存地址
p = &a;
// 下面错误,类型不匹配
p = &b;
int *ip; // ip:一个整型的指针
double *dp; // dp:一个 double 型的指针
float *fp; // fp:一个浮点型的指针
char *ch; // ch:一个字符型的指针
// 指针 ip,dp,fp,ch 都是一个代表内存地址的长的十六进制数,但是地址上存储的数据都是不相同的。
# 混淆解释 1
上面两种使用【*】的场景不同,含义也是不一样的,这也是容易被指针绕晕的一个地方:
int a = 5;
// 第一种 转换 :*(&a) 中的 * 表示获取内存地址中的 数据
printf("a = %d", *(&a)); // 打印a的值5
// 第二种 指针变量:这里的 * 用在【变量声明】的时候,表示这个被 * 修饰的变量是个存储数据对应内存地址变量
int *p; // 定义指针变量p
p = &a;
# 混淆解释 2
其中,指针变量p是单独的变量,只不过存储来变量a的内存地址信息。
int a = 5;
int *p;
p = &a;
/*
其中
*p = a = 5
变量p存储的值是变量a 的内存地址
p = &a
&p != &a
*/
// 指针变量也有自己的内存地址,只不过存储的是地址数据
printf("p 变量的地址: %p \n", &p);
printf("a 变量的地址: %p \n", &a);
// 打印结果
// p 变量的地址: 0x7fff5c7bd670
// a 变量的地址: 0x7fff5c7bd668
# 【 * * p】 指向指针的指针变量
单个星【*】定义中修饰表示指向一个数据对象的指针
两个星【**】定义中修饰表示指向一个数据对象的指针 的 指针
int a = 5;
int *p;
int **p2;
p = &a;
p2 = &p;
/*
p2 = &p = p指针对象的内存地址
*p2 = p = &a = a数据对象内存地址
**p2 = *p = a = 5
*/
# 空指针 NULL
指针声明时,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值,一个0x0的不允许访问的地址。
int *ptr = NULL;
// ptr 的地址是 0x0
// 为确保逻辑完整型,通常需要进行非空判断
if(ptr) /* 如果 p 非空,则完成 */
if(!ptr) /* 如果 p 为空,则完成 */
# 指针的算术运算
前面打印内存地址时看到,内存地址是由 16 进制数据表示的 (比如 0x7ffe7840333c),也即是数据存储的位置由序号标记
既然位置是数据类型,那么存储地址的指针变量同样可以进行算术运算 :++、--、+、-。
下面通过一维数组打印连续对象内存地址:
#include <stdio.h>
const int MAX = ;
int main () {
int var[] = {5, 80, 178, 4};
int i, *ptr;
/* 指针中的数组地址 */
ptr = var;
for ( i = 0; i < MAX; i++) {
printf("下标%d值 = %d; 存储地址 = %p\n", i, *ptr, ptr );
/* 指向下一个位置 */
ptr++;
}
return 0;
}
结果:
$gcc -o main main.c -lm
$main
下标0值 = 5; 存储地址 = 0x7ffec2384170
下标1值 = 80; 存储地址 = 0x7ffec2384174
下标2值 = 178; 存储地址 = 0x7ffec2384178
下标3值 = 4; 存储地址 = 0x7ffec238417c
⚠️ 我们发现,指针++ 移动一次,地址变化为4,刚好是int 类型的字节长度,而不是简单的+1。
因此,指针移动是通过移动对应值类型占用内存字节长度来实现的。
同样指针递减也是一个道理,
递增递减都是连续内存地址,且知道数据类型对应的字节数
# 指针的比较
指针存储是数据信息,可以算术运算,也可以进行比较运算 ==、< 和 > 等等
和运算一样,比较的是地址数据,而不是地址上存储的数据
# 数组如何通过下标取值
上面介绍了指针运算后,我们就很容易理解数组是如何取值的了
int var[] = {10, 100, 200, 300, 400};
⚠️:数组在内存分配的时候是一块连续的内存地址
通过下标3取值 var[2]:
首先var名分配时就存储了首地址,获取指向var[0]的地址,var[0] = 0x7fffba178d64
现在移动2次,因为是int(int 这里4字节算),所以 + 8 ,计算结果是 0x7fffba178d6c
这样就直接获取了var[2] 的地址,在直接 * 运算取值就好了
因此,发现数组由于地址是连续存储的,可以简单计算得到对应的地址,而无需通过遍历等等复杂操作。
# 指针数组
// 值数组,存储数据值
int age[3];
// 指针数组,存储地址
int *ptr[3];
比如:
#include <stdio.h>
int main ()
{
int var[4], *ptr[4]; // 声明一个指针数组ptr
for (int i = 1; i < 4; i++)
{
var[i] = i * 5;
ptr[i] = &var[i]; /* 赋值为整数的地址 */
printf("*ptr下标%d值 = %p; 地址上存储的值 = %d\n", i, ptr[i], *ptr[i]);
}
return 0;
}
输出结果:
$gcc -o main *.c -lm
$main
*ptr下标1值 = 0x7ffd2ee55da4; 地址上存储的值 = 5
*ptr下标2值 = 0x7ffd2ee55da8; 地址上存储的值 = 10
*ptr下标3值 = 0x7ffd2ee55dac; 地址上存储的值 = 15
# 字符串数组
#include <stdio.h>
int main ()
{
// 指向字符的指针数组
const char *names[] = {
"张三",
"李四",
"王五",
};
for (int i = 0; i < 3; i++)
{
printf("姓名 names[%d] = %s\n", i, names[i] );
}
return 0;
}
输出结果:
$gcc -o main *.c -lm
$main
姓名 names[0] = 张三
姓名 names[1] = 李四
姓名 names[2] = 王五
# 函数中使用指针
# 函数指针参数-地址传值
#include <stdio.h>
/* 函数声明 */
void tswap(int *x, int *y);
int main ()
{
/* 局部变量定义 */
int a = 100;
int b = 200;
printf("交换前 x: %d; y: %d\n",a, b);
tswap(&a, &b); //通过指针传递方式
return 0;
}
/* 函数实现 */
void tswap(int *x, int *y)
{
int tmp = *x;
x = y;
y = &tmp;
printf("交换后 x: %d; y: %d\n",*x, *y);
}
结果:数据发生交换
$gcc -o main *.c -lm
$main
交换前 x: 100; y: 200
交换后 x: 200; y: 100
# 函数返回-指针
#
#include <stdio.h>
/* 函数声明 */
int * tswap(int x, int y);
static int total;
int main ()
{
int *t = tswap(100, 200);
printf("函数返回时 total: %d; ret_t: %d\n",total, *t);
*t += 100;
printf("修改返回后 total: %d; ret_t: %d\n",total, *t);
return 0;
}
/* 函数实现 */
int * tswap(int x, int y)
{
total = x + y;
return &total;
}
结果:
$gcc -o main *.c -lm
$main
函数返回时 total: 300; ret_t: 300
修改返回后 total: 400; ret_t: 400
修改对象 t 后,全局变量 total 也同时发生改变,说明他们是引用同一个内存地址
# 函数指针
#include <stdio.h>
void tswap(int x, int y);
static int total;
int main ()
{
// 函数指针声明
void(*p_fun)(int, int) = &tswap; // ⚠️ &tswap 中 & 可以省略
// 通过函数指针p_fun 调用tswap 函数
p_fun(100, 200);
return 0;
}
/* 函数实现 */
void tswap(int x, int y)
{
printf("函数调用 x: %d; y: %d\n", x, y);
}
结果:
$gcc -o main *.c -lm
$main
函数调用 x: 100; y: 200
# 完整示例
#include <stdio.h>
/* 函数声明 */
int * tswap(int *x, int *y);
// 全局变量 total
static int total;
int main ()
{
/* 局部变量定义 */
int a = 100;
int b = 200;
/* 调用函数来交换值
* &a 表示指向 a 的指针,即变量 a 的地址
* &b 表示指向 b 的指针,即变量 b 的地址
*/
printf("交换前 x: %d; y: %d\n",a, b);
// int *t = tswap(&a, &b); // 正常调用函数
// 定义函数指针,并通过函数指针调用函数
int * (*p_fun)(int *, int *) = tswap;
//通过指针传递参数,通过函数指针调用函数
int *t = p_fun(&a, &b);
printf("函数返回时 total: %d; ret_t: %d\n",total, *t);
*t += 100;
printf("修改返回后 total: %d; ret_t: %d\n",total, *t);
return 0;
}
/* 函数实现 */
int * tswap(int *x, int *y)
{
int tmp = *x;
x = y;
y = &tmp;
printf("交换后 x: %d; y: %d\n",*x, *y);
total = *x + *y;
return &total;
}
结果:
$gcc -o main *.c -lm
$main
交换前 x: 100; y: 200
交换后 x: 200; y: 100
函数返回时 total: 300; ret_t: 300
修改返回后 total: 400; ret_t: 400
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数。函数指针是指向函数的指针变量。
整形变量的指针:指向内存地址,只有一个地址
数组指针:指向数组下标0的对象地址
函数指针:指向函数调用的地址,类似OC中的 IMP
# 指向数组的指针
// 声明一维数组
double balance[50];
// 1、数组名是一个指向数组中第一个元素的常量指针
// balance 是一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址
double *p;
double balance[10];
p = balance;
// 2、使用数组名作为常量指针是合法的,下面两种方式等同
int value = balance[4];
int value = *(balance + 4);
通过指针访问数组值:
一旦您把第一个元素的地址存储在 p 中,您就可以使用 p、(p+1)、*(p+2) 等来访问数组元素
#include <stdio.h>
int main () {
/* 带有 5 个元素的整型数组 */
double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
double *p; // p表示变量(指针),*p表示一个double值
int i; // 整形变量i
p = balance; // 数组第一个元素地址指针赋值给p
/*
balance :指向首个数据的地址(指针)
balance+1 :指向第二个数据的地址(指针)
*balance : 首个数据的地址对应的值
*(balance+1) :第二个数据地址对应的值
因为数组地址是连续的,因此可以通过直接地址+1的形式获取下一个值
以下输出数组中每个元素的值 */
for ( i = 0; i < 5; i++ ) {
// 使用指针的数组值
printf("*(p + %d) : %f\n", i, *(p + i) );
// 使用 balance 作为地址的数组值
printf("*(balance + %d) : %f\n", i, *(balance + i) );
}
return 0;
}
// 在上面的实例中,p 是一个指向 double 型的指针,这意味着它可以存储一个 double 类型的变量。一旦我们有了 p 中的地址,*p 将给出存储在 p 中相应地址的值
# 指向结构体的指针
# 定义
1、定义指向结构的指针变量struct_pointer:
struct Books *struct_pointer;
2、结构的指针变量 赋值:
struct_pointer = &Book1;
3、结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:
struct_pointer->title;
# 实例:结构体指针作函数参数
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函数声明 */
void printBook( struct Books *book );
int main( )
{
struct Books Book1; /* 声明 Book1,类型为 Books */
/* Book1 详述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* 通过传 Book1 的地址来输出 Book1 信息 */
printBook( &Book1 );
return 0;
}
// 函数实现:参数是结构体指针,传入是要么是指针变量,要么是结构体地址
void printBook( struct Books *book )
{
printf( "Book title : %s\n", book->title);
printf( "Book author : %s\n", book->author);
printf( "Book subject : %s\n", book->subject);
printf( "Book book_id : %d\n", book->book_id);
}
当上面的代码被编译和执行时,它会产生下列结果:
Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700