首页 > 其他分享 >深入理解指针!!!由浅入深!!1

深入理解指针!!!由浅入深!!1

时间:2024-08-19 13:27:36浏览次数:10  
标签:由浅入深 arr int 地址 深入 数组 printf 指针

1.内存与地址

1.1内存

计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,而内存空间是如何高效的管理的呢?

其实是把内存划分成为一个个的内存单元,每个内存单元的大小取1个字节。

每个内存单元也都一个编号,有了这个内存单元的编号,CPU就可以快速找到一个内存空间。在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新名字:指针

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

内存
0XFFFFFFFF一个字节
0XFFFFFFFE一个字节
一个字节
......
......
......
一个字节
0X00000001一个字节
0X00000000一个字节

补充:计算机中常见的单位

1.  bit - 比特位一个比特位可以存储一个2进制的位1或0
2. byte - 字节位1 byte = 8 bit
3. KB1 KB  = 1024byte
4. MB1MB = 1024KB
5. GB1 GB = 1024MB
6. TB1 TB = 1024GB
7. PB1 PB = 1024TB

1.2究竟该如何理解编址

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址。计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。

我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1。那么⼀根线,就能表示2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。

2.指针变量和地址

2.1 取地址操作符(&)

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

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

                                      &a取出的是a所占4个字节中地址较小的字节的地址。

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

2.2.1 指针变量

#include <stdio.h>
int main() {
	int a = 10;
	int * pa = &a;//取出a的地址并存储到指针变量pa中
	return 0;
}

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

拆解指针类型:pa 左边写的是int *, * 说明pa是指针变量,而前面的int是在说明pa指向的是整型(int)类型的对象

 2.2.2解引用操作符

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

​

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

2.3指针的大小

前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。

结论

32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

                          32位                                                                          64位

3.指针变量类型的意义

3.1 指针的解引用

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

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

3.2指针+-整数

char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

3.3 void*指针

在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算

一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址

4.const修饰指针

4.1 const修饰变量

4.2 const修饰指针变量

const修饰指针变量,可以放在*左边,也可以放在*右边,但是意义不一样

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

结论

const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。

5.指针运算

5.1指针+-整数

数组在内存中是连续存放的,随着数组的增长,地址是由高到低变化的。

                                                              利用指针打印数组

5.2指针-指针

指针+指针没有意义,但指针-指针得到的是指针和指针之间的元素个数

5.3指针的关系运算

6.野指针

概念:野指针就是指针指向的位置是不可知的(随机的,不确定的,没有明确限制的)

6.1野指针成因

6.1.1.指针未初始化

int main() {
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
    return 0;
}

6.1.2.指针越界访问

int main() {
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 11; i++) {
		*(p++) = i;//当指针指向的范围超出数组arr的范围时,p就是野指针
	}
	return 0;
}

6.1.3.指针指向的空间释放

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL.NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址

int* test() {
	int n = 100;
	return &n;
}
int main() {
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

6.2如何规避野指针

6.2.1指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL.NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址

int main()
{
		int num = 10;
	    int* p1 = &num;
	    int* p2 = NULL;
	    return 0;
}

6.2.2小心指针越界

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

6.2.3指针变量不再使用时,及时置NULL,指针使用之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的
时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,
同时使⽤指针之前可以判断指针是否为NULL。

int main()
{
	int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤
	//...
	p = &arr[0];//重新让p获得地址
	if (p != NULL) //判断
	{
		//...
	}
	return 0;
}

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

7.asset断言

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断言“

#include <asset.h>

asset(p!=NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。

assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号

如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。

#define NDEBUG

重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移
除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语
句。
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开
发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,
在 Release 版本不影响⽤⼾使⽤时程序的效率。

8. 指针的使用和传址调用

8.1 strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。

函数原型 size_t strlen(const char* str);

#include<assert.h>
#include<stdio.h>
size_t my_strlen(const char* arr) {
	assert(arr != NULL);
	size_t count = 0;
	while (*arr!='\0') {
		count++;
		arr++;
	}
	return count;
}
int main() {
	size_t len = my_strlen("abcdef");
	printf("%zd", len);
	return 0;
}

8.2 传值调用和传址调用

写一个函数,交换两个整型变量的值

一番思考,可能写出这样的代码

我们发现其实没产生交换的效果,这是为什么呢?

这是因为:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。这种函数的调用方式叫:传值调用

那怎么办呢?我们可以利用指针

这里调用的Swap函数就时将变量的地址传递给了函数,这种函数的调用方式叫:传址调用

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。

9.数组名的理解

我们可以发现数组名和数组首元素地址一摸一样,其实数组名就是数组首元素(第一个元素)的址。那有同学就会有疑问?数组名如果是数组首元素的地址,那sizeof(arr)输出结果应该是4/8才对。

其实数组名就是数组首元素(第一个元素)的地址,但是有两个意外

sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小单位是字节

&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址还是有区别的)

除此之外,任何地方使用数组名,数组名都表示首元素的地址


这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。

10.使用指针访问数组

int main() {
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int* p = arr;
	for (i = 0; i < sz; i++) {
		scanf("%d", p + i);//也可以写成arr+i
	}
	for (i = 0; i < sz; i++) {
		printf("%d", p[i]);//也可以写成*(p+i),*(arr+i),arr[i]
	}
	return 0;
}

11一维数组传参的本质

我们发现在函数内部是没有正确获得数组的元素个数。
这就要学习数组传参的本质了,数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组⾸元素的地址。
所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写
sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函
数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

11.冒泡排序

12.二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?

这就是二级指针

int main() {
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
    return 0;
}

对于二级指针的运算有:

int main() {
    int a = 20; 
    int **ppa = &a;//等价于pa = &a;
    int **ppa = 30;//等价于*pa = 30;等价于a = 30;
}

13.指针数组

类比一下,整型数组是存放整型的数组,字符数组是存放字符的数组,那指针数组呢?是存放指针的数组。

 指针数组的每个元素是地址,又可以指向一块区域

14.指针数组模拟二维数组

15.字符指针变量

int main() {
	//字符指针变量一般使用方法
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
    //另外一种使用方法
	const char* pstr = "hello bit";
	//不是把字符串hello bit放在字符指针pstr中,本质上是把字符串hello bit首地址放到了pstr中
	printf("%s\n", pstr);
	return 0;
}

例题

str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,
当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

16.数组指针变量

16.1数组指针变量是什么

数组指针变量是指针变量,存放的是数组的地址,能够指向数组的指针变量。

int (*p) [10]

p先和*结合,说明p是⼀个指针变量,然后指向的是⼀个⼤⼩为10个整型的数组。所以
p是⼀个指针,指向⼀个数组,叫 数组指针。

这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合

16.2数组指针变量如何初始化

int main() {
	int arr[10] = { 0 };
	int(*p) = &arr;
	return 0;
}

16.3二维数组传参本质

首先我们再次理解⼀下⼆维数组,⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类
型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址
,那么形参也是可以写成指针形式的。如下:

17.函数指针变量

17.1 函数指针变量的创建

函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。

由代码可以得知:函数是有地址      的,函数名就是函数的地址,也可  以通过&函数名的方式获得函数的地址。

//函数指针变量的写法
void test() {
	printf("haha\n");
}
void (*p1)() = test;
void (*p2)() = &test;

int Add(int x, int y) {
	return x + y;
}
int(*p3)(int, int) = Add;
int(*p4)(int, int) = &Add;

17.2 函数指针变量的使用

17.3 两段有趣的代码 

 1.(*(void (*)()) 0) ();//函数调用
(void(*)())//函数指针类型
(void (*)()) 0)//将0强制类型转换成函数指针类型
(*(void (*)()) 0) ()//调用0地址处放的这个函数

2.void(*signal(int, void(*)(int)))(int);//函数声明
//声明的函数名字叫:signal
//signal函数有两个参数,第一个参数是int
// 第二个参数的类型是void(*)(int)的函数指针类型,该指针可以指向一个函数,指向的函数参数是int,返回类型是void 
//signal函数的返回类型是void(*)(int),该指针可以指向一个函数,指向的函数参数是int,返回类型是void

void(*)(int) signal(int, void(*)(int));//编译器不支持这种写法

18 typedef 关键字

typedef 是用来类型重命名的,可以将复杂类型简单化

例如:

1.unsigned int 写起来不方便可以重命名为uint

     typedef unsigned int uint;

2.指针类型重命名将int*重命名为ptr_t

     typedef int* ptr_r;

但是对于数组指针和函数指针稍微有点区别

3.int(*)[5]重命名为parr_t

     typedef int(*parr_t)[5];//新的类型名必须在*的右边

4.void(*)(int)重命名为pf_t

     typedef void(*pf_t)(int);//新的类型名必须在*的右边

那我们要简化void(*signal(int, void(*)(int)))(int);

     typedef void(*pfun_t)(int);

     pfun_t signal(int, pfun_t);

19.函数指针数组

19.1函数指针数组是什么

函数指针数组:int (*parr1[3])();

parr1先和[ ]结合,说明parr1是数组,数组的内容是int (*)()类型的函数指针。

19.2函数指针数组的用途:转移表

举例:计算器的一般实现

20.指针小总结:

一级指针:char* p1;              int* p2;

二级指针:char**pp1=&p1;   int**pp2=&p2

数组指针---指向的是数组

int arr[5];   int (*p)[5]=&arr;

函数指针---指向的是函数

char*test(int n,char*s){

}

char*(*pf)(int,char*)=test;//pf是函数指针变量

 指针数组:char*arr[5];   int*arr2[5];    float*arr4[6];     double*arr3[9]

函数指针数组: char* (* pfArr[4] (int,char*);

扩展:指向函数指针数组的指针

char*(*(*p)[4])(int,char*)=&pfArr//取出的是函数指针数组的地址,p就是指向函数指针数组的指针

              

#include<stdio.h>
int add(int a,int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a *b;
}
int div(int a, int b) {
	return a / b;
}
int main() {
	int input = 0;
	int x, y = 0;
	int ret = 0;
	do{
	printf("*******************************\n");
	printf("****1.add           2.sub******\n");
	printf("****3.mul           4.div******\n");
	printf("*******************************\n");
	printf("请选择");
	scanf("%d", &input);
	switch (input) {
	case 1:
		printf("请输入操作数\n");
		scanf("%d %d", &x, &y);
		ret = add(x, y);
		printf("ret=%d\n", ret);
		break;
	case 2:
		printf("请输入操作数\n");
		scanf("%d %d", &x, &y);
		ret = sub(x, y);
		printf("ret=%d\n", ret);
		break;
	case 3:
		printf("请输入操作数\n");
		scanf("%d %d", &x, &y);
		ret =mul(x, y);
		printf("ret=%d\n", ret);
		break;
	case 4:
		printf("请输入操作数\n");
		scanf("%d %d", &x, &y);
		ret = div(x, y);
		printf("ret=%d\n", ret);
		break;
	case 0:
		printf("退出程序\n");
		break;
	default:
		printf("选择错误,请重新选择\n");
		break;
	}
	} while (input);
	return 0;
}

使用函数指针实现:

int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}
int div(int a, int b) {
	return a / b;
}
int main() {
	int input = 0;
	int x, y = 0;
	int ret = 0;
	int(*p[5])(int x, int y) = { NULL,add,sub,mul,div };
	do {
		printf("*******************************\n");
		printf("****1.add           2.sub******\n");
		printf("****3.mul           4.div******\n");
		printf("********   0.exit   ***********\n");
		printf("*******************************\n");
		printf("请选择");
		scanf("%d", &input);
		if (input > 0 && input <= 4) {
			printf("请输入操作数\n");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("%d\n", ret);
		}
		else if (input == 0) {
			printf("退出计算器\n");
		}
		else {
			printf("输入有误\n");
		}
	} while (input);
	return 0;
}

21.回调函数

回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数
时,被调⽤的函数就是回调函数

int add(int a,int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a *b;
}
int div(int a, int b) {
	return a / b;
}
void calc(int(*pf)(int, int)){
	int ret = 0;
	int x, y = 0;
	printf("请输入操作数\n");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("ret=%d\n", ret);
}//使用了回调函数
int main() {
	int input = 0;
	do{
	printf("*******************************\n");
	printf("****1.add           2.sub******\n");
	printf("****3.mul           4.div******\n");
	printf("*******************************\n");
	printf("请选择");
	scanf("%d", &input);
	switch (input) {
	case 1:
		calc(add);
		break;
	case 2:
		calc(sub);
		break;
	case 3:
		calc(mul);
		break;
	case 4:
		calc(div);
		break;
	case 0:
		printf("退出程序\n");
		break;
	default:
		printf("选择错误,请重新选择\n");
		break;
	}
	} while (input);
	return 0;
}

22.qsort使用举例

22.1 使用qsort函数排序整型数据

1.qsort----用来排序的,库函数,直接可以用来排序数据,底层使用的是快速排序的方式

2.qsort函数

void qsort(void* base,//指针,指向的是待排序数组的第一个元素
    size_t num,//是base指向的待排序数组的元素个数
    size_t size,//是base指向的待排序数组的元素的大小
    int (*compar)(const void*, const void*)//函数指针
);

qsort函数的使用者——明确知道要排序的是什么数据,这些数据应该如何比较,所以提供两个元素的比较函数

22.1 使用qsort函数排序整型数据

                

#include<stdlib.h>
int int_cmp(const void* p1, const void* p2) {
    if (*(int*)p1 > *(int*)p2) {
        return 1;
    }
    else if (*(int*)p1 == *(int*)p2) {
        return 0;
    }
    else {
        return -1;
    }
    //return (*(int*)p1) - (*(int*)p2);
    //qsort的使用者得实现一个比较函数
}
int main() {
    int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
    int i = 0;
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
        printf("%d", arr[i]);
    }
    printf("\n");
	return 0;
}

22.2 使用qsort函数排序结构体

补充:

int strcmp(const char* str1, const char* str2);

标准规定:第一个字符串大于第二个字符串,则返回大于0的数字

                  第一个字符串等于第一个字符串,则返回0

                  第一个字符串小于第一个字符串,则返回小于0的数字

22.3 qsort函数的模拟实验

采用冒泡的方式模拟实现qsort函数

int int_cmp(const void* p1, const void* p2) {
	return (*((int*)p1) - *((int*)p2));
}
void _swap(void* p1,void*p2,int size) {
	int i = 0;
	for (i = 0; i < size; i++) {
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}

}
void bubble(void *base,int count,int size,int(*cmp)(void*,void*)) {
	int i = 0; 
	int j = 0;
	for (i = 0; i < count-1; i++) {
		for (j = 0; j < count - 1 - i; j++) {
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size)>0) {
				_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main() {
	int arr[] = { 1,3,5,7,9,2,4,6,8,0 };
	int i = 0;
	bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
		printf("%d", arr[i]);
	}
	printf("\n");
	return 0;
}

22.sizeof和strlen的对比

22.1 sizeof

sizeof单目操作符,sizeof计算变量所占内存内存空间⼤⼩的,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的⼤⼩。
sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。

int main()
{
	int a = 10;
	printf("%zd\n", sizeof(a));//4
	printf("%zd\n", sizeof a);//4
	printf("%zd\n", sizeof(int));//4

	return 0;
}

22.2 strlen

strlen 是C语⾔库函数,功能是求字符串⻓度。函数原型如下:size_t strlen ( const char * str );

统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。
 

int main()
{
	char arr1[3] = { 'a', 'b', 'c' };
	char arr2[] = "abc";
	printf("%d\n", strlen(arr1));//要在内存中找到\0为止,所以是随机值
	printf("%d\n", strlen(arr2));//3

	printf("%d\n", sizeof(arr1));//3
	printf("%d\n", sizeof(arr2));//4
}

22.3 sizeof和strlen的对比23.数组和指针的笔试题解析

23.1一维数组

23.2字符数组

代码1

代码2

代码3

代码4

代码5

代码6


22.3二维数组


数组名的意义:

1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小

2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址

3.除此之外所有的数组名都表示首元素的地址

23. 指针运算笔试题解析

23.1 题目一

23.2 题目二

23.3题目三

23.4 题目四

//假设环境是x86环境,程序输出的结果是啥?

%p打印时直接将补码当成地址打印,在x86的环境下,4个2进制位转换成一个16进制位

输出结果为FFFFFFFC,-4

23.5 题目五

&aa是取出数组的地址,+1跳过一整个数组;aa是二维数组首元素的地址,就是第一行的地址,+1跳过一行。

23.6 题目六

%s是打印字符串,给一个地址,从这个地址向后打印字符串,直到\0

23.7 题目七


先算++cpp,之前算出cpp指向c+2的空间,则++cpp指向c+1的空间,*++cpp得到值c+1,在--值变成c,在解引用*,得到字符串“ENTER"的首地址,再+3,得到ER


cpp[-2]也就是*(cpp-2),cpp原先指向c+1的空间,现在指向c+3 的空间,*解引用得到字符串”FIRST“的首地址,+3得到ST


只有自增自减,才能改变cpp的值,所以现在cpp一开始是指向c+1空间的,cpp[-1][-1]也就是

*(*(cpp-1)-1),cpp-1指向空间c+2,*对其解引用得到其值,-1得到c+1,再*解引用得到字符串”NEW“的地址,再+1,得到EW


标签:由浅入深,arr,int,地址,深入,数组,printf,指针
From: https://blog.csdn.net/2302_80121989/article/details/140924615

相关文章

  • 学懂C++(三十七):深入详解C++网络编程开发
            目录一、网络编程基础概念与原理1.1套接字(Socket)1.2IP地址和端口1.3TCP/IP协议二、C++网络编程核心技术2.1套接字编程2.1.1创建套接字2.1.2绑定地址2.1.3监听和接受连接2.1.4发送和接收数据三、C++网络编程高级技术3.1异步I/O3.2多线......
  • 学懂C++(三十八):深入详解C++网络编程:套接字(Socket)开发技术
    目录一、概述与基础概念1.1套接字(Socket)概念1.2底层原理与网络协议1.2.1网络协议1.2.2套接字工作原理二、C++套接字编程核心技术2.1套接字编程的基本步骤2.2套接字编程详细实现2.2.1创建套接字2.2.2绑定地址2.2.3监听和接受连接(服务端)2.2.4客户端连接2.......
  • 深入浅出LLM基础:探索Embedding模型的核心原理与应用
    Embedding模型概览Embeddings是自然语言处理技术中很重要的基石。它有很多种模型,从GloVe、word2vec、FastText、Bert、RoBERTa、XLNet、OpenAIada、GoogleVertexAITextEmbeddings、AmazonSageMakerTextEmbeddings和Cohere。每种模型都有优劣,如何去分析这些Embeddin......
  • 深入了解SOCKS5代理:全面指南
    在现代数字世界中,互联网隐私和性能至关重要。SOCKS5代理服务器因其独特的功能和灵活性,成为提高网络安全和性能的热门选择。本文旨在深入探讨SOCKS5代理服务的工作原理、优势及其在实际应用中的表现,帮助您快速了解如何最大限度地利用这一工具。如何理解SOCKS5代理服务器SOCKS......
  • AI大模型神作推荐:深入浅出,从基础到前沿的全面学习!
    今天给大家推荐一本大模型神书,就是这本:《大语言模型:基础与前沿》,本书深入阐述了大语言模型的基本概念和算法、研究前沿以及应用,涵盖大语言模型的广泛主题,从基础到前沿,从方法到应用,不仅涵盖了经典的语言模型知识,还重点介绍了最新的研究成果和技术进展。适宜人群本书内容全......
  • 深入理解Kerberos:现代网络身份验证的基石
    在当今的分布式计算环境中,网络安全已成为企业和组织最为关注的问题之一。为了确保用户在网络上的身份真实性并保护敏感数据,采用安全且有效的身份验证机制至关重要。而Kerberos,作为一种经典且广泛应用的网络身份验证协议,正是解决这一问题的核心工具之一。一、什么是Kerberos......
  • 深入解析Spring Boot中多环境配置的实现
    深入解析SpringBoot中多环境配置的实现在现代软件开发中,应用程序往往需要在不同的环境中运行,例如开发环境、测试环境、生产环境等。每个环境可能有不同的配置需求,如数据库连接、日志级别、服务器地址等。SpringBoot提供了强大的多环境配置支持,使得开发者能够轻松管理不同......
  • 总结指针数组与数组指针的区别
    1、指针数组1-1、定义指针数组是一个数组,其元素是指针。这意味着数组的每个位置都存储了一个指针,这些指针可以指向任何类型的数据(包括其他数组、结构体等)。1-2、类型如果有一个指向整数的指针数组,其类型可能是 int*arr[N];,这里 arr 是一个数组,包含 N 个 int* 类型......
  • 深入理解 `@DateTimeFormat` 和 `@JsonFormat` 注解
    前言在Java应用程序中,处理日期和时间是一个常见的需求。无论是从数据库读取还是通过API接收数据,正确的日期和时间格式都是确保应用正确运作的关键因素。本文将深入探讨两个常用的注解——@DateTimeFormat和@JsonFormat——以及它们如何帮助我们在Spring和使用Jackson库的应......
  • 深入探究 Java 中的单元测试 Mock 技术
    在软件开发中,单元测试是确保代码质量和稳定性的重要手段。而Mock技术在单元测试中扮演着至关重要的角色,它能够帮助我们隔离外部依赖,更有效地对单个模块进行测试。本文将深入探讨Java中的单元测试Mock技术。一、单元测试与Mock技术概述单元测试是对软件中的最小可测试......