C语言--- 指针(1)

目录

一.内存和地址

1.1内存

1.2理解编址

二.指针变量和地址

2.1取地址操作符

2.2指针变量和解引用操作符(*)

2.2.1指针变量

2.2.2指针类型

2.2.3解引用操作符 

2.3指针变量的大小

三.指针变量类型的意义

3.1指针的解引用

3.2指针+-整数 

3.3void*指针

四.const修饰指针

4.1const修饰变量

4.2const修饰指针变量

五.指针运算

1.指针+-整数

2.指针-指针

3.指针的关系运算

六.野指针

6.1造成野指针的三种原因

1.指针为初始化

2.指针越界访问

3.指针指向的空间释放

6.2规避野指针

6.2.1指针初始化

 6.2.2避免指针越界

6.2.3及时置为NULL

6.2.4避免返回局部变量的地址

七.assert断⾔

八.指针的使⽤和传址调⽤

8.1strlen

8.2传址调用


一.内存和地址

1.1内存

CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。

计算机把内存划分为一个个的内存单元,每个单元的大小取一个字节。

计算的常见单位:

bit    -   比特位
byte  -  字节1byte = 8bit
KB1KB = 1024byte
MB1MB = 1024KB
GB1GB = 1024MB
TB1TB = 1024GB
PB1PB = 1024TB

每一个比特位可以存放一个2进制的数字0或者1。

一个字节空间可以存放8个比特位。

为了方便CPU快速寻找到每一个空间,每个内存单元都有一个编号。

在计算机中我们把内存单元的编号称为地址。

C语言中我们给地址起了一个新名字:指针。

所以我们可以理解为:内存单元的编号==地址==指针

1.2理解编址

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,⽽因为内存中字节很多,所以需要给内存进⾏编址。
⾸先,必须理解,计算机内是有很多的硬件单元,⽽硬件单元是要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。但是硬件与硬件之间是互相独⽴的,那么如何通信呢?答案很简单,⽤"线"连起来。⽽CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也⽤线连起来。 不过,我们今天关⼼⼀组线,叫做地址总线
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么
⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含
义,每⼀种含义都代表⼀个地址。 地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。

二.指针变量和地址

2.1取地址操作符

在C语言中,创建变量其实就是向内存申请空间

比如:

上述代码中创建了变量a,内存中申请了4个字节,用于存放整数10,其中每个字节都是有地址的,如图中的内存窗口。

为了得到a的地址,我们就要用到操作符(&)————取地址操作符。

我们很容易发现&a取出的是a所占字节中较小的字节的地址。

虽然整形变量占了四个字节,但是我们只需知道第一个字节,就可以找到四个字节的数据。

2.2指针变量和解引用操作符(*)

2.2.1指针变量

我们通过取地址操作符(&)拿到的是一个数值,这个数值有时候也是需要储存起来的,方便我们使用,那我们把这样的地址存放在指针变量中。

比如:

#include<stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    
    return 0;
}

 指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量的值都会理解为地址。

2.2.2指针类型

我们看到的pa的类型是int*。

*是在说明pa是指针变量,而int 在说明pa指向的是整形类型的的对象。

 

 例如:

//char类型的变量ch,ch的地址
    char ch = 'a';
    char* pch = &ch;

2.2.3解引用操作符 

我们将地址保存起来,是为了未来使用的。

在C语言中,我们拿到地址(指针),就可以通过地址(指针)找到地址(指针)所指向的对象,这时候我们就需要用到操作符---解引用操作符(*)。

#include<stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    *pa = 0;
    printf("%d\n",a);
    return 0;

}
上⾯代码中就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0.

2.3指针变量的大小

32位的机器有32根地址总线,每根地址总线的出来的电信号转换成数字信号后是1或者0,那我们把32根地址总线的产生2进制的序列当作一个地址,那么一个地址就是32位比特位,需要4个字节才能存放。即指针变量的大小是4个字节

同理如果是64位的机器,指针变量的大小就是8个字节。

#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
     printf("%zd\n", sizeof(char *));
     printf("%zd\n", sizeof(short *));
     printf("%zd\n", sizeof(int *));
     printf("%zd\n", sizeof(double *));
     return 0;
}

在X86的环境小输出的结果就是

4

4

4

4

在X64的环境下输出的结果就是

8

8

8

注意:指针变量的大小与类型是无关的,只要是指针类型的变量。在相同平台下,大小就是相同的。

三.指针变量类型的意义

3.1指针的解引用

 在上面两个代码中我们可以看到,代码1会将n的四个字节全部改为0,但是代码2只是将n的第一个字节改为0。

指针的类型决定了,对指针解引用的时候有多大的权限(一次可以操作几个字节)

如:char*的指针解引用就只能访问1个字节,而int*的指针解引用就能访问四个字节

3.2指针+-整数 

观察下面这段代码

#include <stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	return 0;
}

输出结果是:

我们很容易发现,char*类型的指针变量加1跳过一个字节,而int*类型的指针变量加1跳过的是4个字节

即指针的类型决定了指针加一减一向前或者向后走一步的距离有多大。

3.3void*指针

在指针类型中,有一种特殊的类型是void* 类型的指针,可以理解为无具体类型的指针(泛型指针),这种类型的指针可以接受任何类型的地址。但是也有局限,void*类型的指针不能直接进行指针的加减整数和解引用的运算。

#include<stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    char* pc = &a;

    return 0;
}

在上面的代码中,将int类型的变量的地址赋值给char*类型的指针变量,编译器会报错。

但是使用void*类型的指针就不会。

一般void*类型的指针是使用函数的参数的部分,用来接受不同数据类型的地址。这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。

四.const修饰指针

4.1const修饰变量

当用const修饰变量的时候,变量的值是不能被修改的,比如下面的代码

#include<stdio.h>
int main()
{
    int n = 10;
    const int m = 10;
    return 0;
}

n的值是可以改变的,而const的值是不能改变的。

但是我们可以绕过n,使用n的地址,去修改n,就可以做到修改n的值。

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

这样我们就将n的值从10变成了20;

4.2const修饰指针变量

观察下面四个代码

#include <stdio.h>
//代码1
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test2()
{
	//代码2
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?
}
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?
}
int main()
{
	//测试⽆const修饰的情况
	test1();
	//测试const放在*的左边情况
	test2();
	//测试const放在*的右边情况
	test3();
	//测试*的左右两边都有const
	test4();
	return 0;
}

 test2 和3和 4()会报错。

结论const在修饰指针变量的时候,

如果const放在*的左边,修饰的是指针说指向的内容,确保指针所指向的内容不能被修改。但是指针变量可以改变。

如果const放在*的右边,修饰的是指针变量本身,确保指针变量的内容是不能被修改的,但是指针所指向的内容,可以通过指针进行修改。

五.指针运算

  指针运算有三种:


1.指针+-整数

我们知道数组在内存中是连续存放的,我们只要知道一个元素的地址,就能找到数组中的所有元素。

#include <stdio.h>
//指针+- 整数
int main()
{
     int arr[10] = {1,2,3,4,5,6,7,8,9,10};
     int *p = &arr[0];
     int i = 0;
     int sz = sizeof(arr)/sizeof(arr[0]);
     for(i=0; i<sz; i++)
     {
         printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
     }
     return 0;
}

 p是下标为0的数组元素的地址,p+i就是下标为i的元素的地址,再解引用就可以知道数组的元素是什么了

我们得到了两个等式

数组名就是首元素的地址 

p=arr;
arr[i] = *(p + i) = *(i + p) = i[arr];

我们可以进行适当验证!

 

当然不建议这样写!

2.指针-指针

#include<stdio.h>
int my_strlen(char* s)
{
    char* p = s;
    while(*p != '\0')
      p++;
      return p-s;
      
}


int main()
{
    printf("%d\n",my_strlen("abc"));
    return 0;
}

 p-s就是指针减指针,算出来的是字符串长度。

3.指针的关系运算

//指针的关系运算
#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

六.野指针

野指针就是指向的位置是不可知的,(随机的,不正确的)。

6.1造成野指针的三种原因

1.指针为初始化

#include<stdio.h>
int main()
{
    int* p;
    *p = 10;
    return 0;
}

此时的局部变量未初始化,默认为随机值。 

2.指针越界访问

int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 11; i++)
	{	
		*(p++) = i;
	}
	return 0;
}

 当指针指向的范围超出的数组arr的范围时,p就变成了野指针。 

3.指针指向的空间释放

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

由于局部变量存在生命周期,虽然我们可以得到输出结果是100,但是n作为局部变量已经被销毁了,此时就是指针指向的空间释放了 

6.2规避野指针

6.2.1指针初始化

如果我们明确知道指针指向的哪里就直接赋值地址,如果不知道指针应该指向哪里,我们就可以将指针赋值为NULL.

NULL是C语言中定义的一个常识标识符常量,值为0,0也是地址,这个地址是无法使用的,读写这个地址会报错。

#include <stdio.h>
int main()
{
	int num = 10;
	int* p1 = &num;
	int* p2 = NULL;

	return 0;
}

 6.2.2避免指针越界

一个程序向内存申请了空间,通过指针也就只能访问固定的空间,不能超出访问范围,超出了就是越界访问。

6.2.3及时置为NULL

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不问,
同时使⽤指针之前可以判断指针是否为NULL。我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。

6.2.4避免返回局部变量的地址

比如造成野指针的第三个例子

七.assert断⾔

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。assert() 的使⽤对程序员是⾮常友好的,使用assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h>语句的前⾯,定义⼀个宏 NDEBUG 。然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。

八.指针的使⽤和传址调⽤

8.1strlen

库函数strlen的作用是求字符串的长度,统计字符串中\0之前的字符的个数。

int my_strlen(const char* str)
{
	int count = 0;
	assert(str);
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);
	return 0;
}
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直 到 \0 就停⽌。

8.2传址调用

#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap1(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

如果我们要调换a和b的值,我们观察上面的代码,会发现输出结果没变

 因为形式参数是实际参数的一份临时拷贝,形参的改变不会改变实际参数的值。

这种Swap1函数的使用是传值调用,所有我们不能实现这样的效果!

而传址调用可以

比如:
 

#include <stdio.h>
void Swap2(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int a = 10;
	int b = 10;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap2(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛应用于系统编程和嵌入式开发的高级编程语言。函数指针C语言中的一个重要概念,它可以指向函数,并且可以通过函数指针来调用函数。快速排序算法是一种常用的排序算法,它的核心思想是通过分治的策略将一个大问题分解为多个小问题,并通过递归的方式解决这些小问题。 下面是C语言中使用函数指针实现快速排序算法的示例代码: ```c #include <stdio.h> // 交换两个元素的值 void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } // 快速排序的分区函数 int partition(int arr[], int low, int high) { int pivot = arr[high]; // 选取最后一个元素作为基准 int i = (low - 1); // 定义一个指针,用于指向小于基准的元素 for (int j = low; j <= high - 1; j++) { if (arr[j] < pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); return (i + 1); } // 快速排序函数 void quickSort(int arr[], int low, int high) { if (low < high) { int pi = partition(arr, low, high); // 将数组分区,并获取分区点的位置 quickSort(arr, low, pi - 1); // 对分区点左边的子数组进行快速排序 quickSort(arr, pi + 1, high); // 对分区点右边的子数组进行快速排序 } } // 打印数组元素 void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int arr[] = {10, 7, 8, 9, 1, 5}; int n = sizeof(arr) / sizeof(arr[0]); printf("原始数组:"); printArray(arr, n); quickSort(arr, 0, n - 1); printf("排序后的数组:"); printArray(arr, n); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值