首先感谢大家对我的支持,大家的支持是我持续更新的动力哦!
1.地址与内存
在讲内存和地址之前,我们想有个⽣活中的案例:
假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩, 如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
一楼:101,102,103...
二楼:201,202,203...
三楼:301,302,302...
...........
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
地址就如同房间号一般,可以快速找到我们希望找到的内容。每个内存单元也都有⼀个编号(这个编号就相当 于宿舍房间的⻔牌号),有了这个内存单元的编 号,CPU就可以快速找到⼀个内存空间。我们也可以这么理解:
内存单元的编号 == 地址 == 指针
2.指针变量和地址
int * p = &a; //p是一个变量(指针变量)
注:“*”是说明p是指针变量;“int”在说明p指向的对象是int类型。
我们现在在引入一个操作符:解引用操作符(间接访问操作符)‘*’。
int a = 10;
int * p = &a;
//*p = 10 <--> *&a = 10
小知识:指针变量的大小与(地址总线根数)有关,与(地址对象的类型)无关。
故:32位平台下地址是32个bit位,指针变量大小为4个字节(X86) 64位平台下地址是64个bit位,指针变量大小为8个字节(X64)
3.指针类型变量的意义
指针类型 决定了指针进行解引用操作符的时候访问几个字节,也就是决定了指针的访问权限。
char* ---> 指向字符的指针
short*---> 指向短整型的指针
int* ---> 指向整型的指针
float* ---> 指向单精度浮点型的指针
void* ---> 无具体类型的指针(泛型指针)<多用于函数参数的部分>
int a = 10;
void* p = &a;
*p = 20;//error
p = p+1;//error
故,对void*指针,可以用其存放地址,但不能够进行运算。
4.const修饰指针
举个例子:
const int a = 10;//a具有了常属性(不能被修改了)
<a为常变量,其本质还是变量>
const修饰符有两个位置可以使用,分别是解引用符‘*’的前与后,如下:
一、
int constr *p = &a;
p = &b;//OK
*p=100;//ERROR
const修饰指针变量时, 放在*的左边,限制的是指针指向的内容,不能通过指针来修改,但可以修改指针变量本身;
二、
int * const p = &a;
p = &b;//ERROR
*p = 100;//OK
const修饰指针变量时, 放在*的右边,限制的是指针变量本身,其不能再指向其他变量,可通过指针变量修改指针变量指向的内容。
5.指针的运算
a.指针-指针:得到的为两个指针间的元素个数。(指针必须指向同一个数组)
b.指针+-数字:因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
6.野指针
a.指针指向的位置是不可知的;
b.数组越界;
c.指针指向的空间释放。
这三点是野指针发生的条件。那我们就应当晓得解决办法,如下:
1.明确知道指针应该指向哪里,就给初始化一个明确的地址; 2.如果现在还不知道应该指向哪里,那就初始化为NULL。
7.assert断言
大致用法
#include<assert.h> //使用assert函数必须添加的头文件
#define NDEBUG // 关闭assert断言
assert( 表达式 ); //只有表达式成立时,程序才可以运行,若不符合表达式,程序将终止运行
8.指针的传值调用和传址调用
一、strlen函数的功能与模拟实现
1.库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。 【其头文件为: #include <string.h>】
2.模拟实现(a.指针减指针型 b.计数器型 c.递归型 )
a.指针减指针型
#include <stdio.h>
#include <assert.h>size_t my_strlen(const char* arr)
{
assert(arr != NULL); //判断数组arr是否是空指针
char* ret = arr; //记录数组arr
while (*arr++ != '\0')
{
;
}
arr -= 1; //记录数组arr的最后一个元素的地址位置
return arr - ret; //数组末地址减去数组首地址,为数组元素个数
}int main()
{
char arr[] = "abccdef";
int ret = my_strlen(arr);
printf("%d", ret);
return 0;
}b.计数器型
size_t my_strlen(const char* arr)
{
assert(arr);
int count = 0; // 设置计数器
while (*arr != '\0')
{
count++; //计算数组arr中元素个数
arr++;
}
return count;
}c.递归型
size_t my_strlen(const char* arr)
{
assert(arr);
char* ret = arr;
if (*arr != '\0')
{
return 1 + my_strlen(++arr); //每次递归时加1,并减少数组arr中1个元素个数
}
else {
return 0;
}
}
二、传址调用
我们以交换数字举例
#include <stdio.h>
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = 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;
}
首先看输出结果:
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
三、传值调用
#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;
}
运行结果
我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是 x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤ 的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调⽤。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。
9.数组名的理解
数组名就是数组⾸元素(第⼀个元素)的地址。
但是有两个例外:
• sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表示整个数组,计算的是整个数组的大小, 单位是字节
• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的) 除此之外,任何地⽅使⽤数组名,数组名都表示⾸元素的地址。
10.冒泡排序
冒泡排序的核心思想就是:两两相邻元素进行比较。
11.二级指针
eg;
int a = 10;
int *p = &a; // p是1级指针
int **pp = &p; // pp是2级指针
解析 int * * pp = &p;
"int *"在说明pp执行的p的类型为"int *";
""*pp"中的‘*’说明pp是指针变量。
12.指针数组与数组指针
一、举例区分
int * p1[10]; // p1是指针数组 ———存放指针的数组;
int (*p2)[10]; //p2是指针变量,指向的是数组 ————数组指针。
二、用指针数组模拟二维数组
eg:
#include <stdio.h>
int main()
{
int arr1[ ] = { 1, 2, 3, 4, 5 };
int arr2[ ] = { 2, 3, 4, 5, 6 };
int * arr[ ] = { arr1, arr2 }; // 指针数组arr 可模拟二维数组;
// 打印数组
int i = 0;
int j = 0;
for( i = 0 ; i < 2 ; i++ )
{
for( j = 0 ; j < 5 ; j++ )
{
printf( "%d", arr[i][j] ); // arr[i][j]等价于*( *( arr * i) + j );
}
}
return 0;
}
三、数组的地址要存放在数组指针变量当中
int (*p) [10] = &arr;
四、顺序打印正整数的每一位(递归法)
#include <stdio.h>
void Print(int n)
{
if (n > 9)
{
Print(n / 10);
printf("%d ", n % 10);
}
else {
printf("%d ", n % 10);
}}
int main()
{
//顺序打印正整数的每一位
int i = 0;
scanf("%d", &i);
Print(i);
return 0;
}
13.字符数组变量
char arr [10] = "abcdef";
类型1.
char * p1 = arr ; <p1为首元素地址>
* p1 = 'w' ;
printf("%s", p1); // "wbcdef";<%s打印字符串时,需要提供起始地址>
类型2.
(const) char * p2 = "abcdef"; // 常量字符串 ; <p2为首字符地址>
* p2 = 'w'; // ERROR;
由类型1得,字符数组内容可变;
由类型2得,常量字符串不可被修改。
14.二维数组传参的本质(真正原理)
#include <stdio.h>
void print ( int (*arr)[5] , int r, int c)
{
int i = 0;
int j = 0;
for( i = 0;i < r; i++ )
{
for( j = 0; j < c; j++ )
{
printf( "%d ",*(*(arr + i) + j ) );
}
printf("\n");
}
}
int main()
{
int arr[5][2] = {{1,2},{2,3},{3,4},{4,5},{5,6}};
print( arr, 5, 2);
return 0;
}
15.函数指针变量
一、函数指针变量的解析
二、函数指针变量的使用
eg:
int Add ( int x, int y)
{
return x + y;
}
int main( )
{int (*pf)(int, int ) = &Add;
printf("%d\n", (*pf3)(2, 3)); // 5
printf("%d\n", pf3(3, 5)); // 8
return 0;
}
三、有趣的代码(能力拔高)
( *(void (* )( ) ) 0 ) ( );
解析:
1.void (* )( ) ——>函数指针类型;
2.( void (* )( ) ) ——> 强制类型转换;
3.*( void (* )( ) ) 0 ——>将0强制类型转换为“void (* )( )”的函数指针类型;
4.其意味着假设0地址处放着无参,返回类型是void的函数。
16.typedef关键字
举例运用
1.
typedef int* pint_t // 之后可以用“pint_t”来替代“int*”;
2.
typedef int (*pf_t) [5] // "pf_t" <==> "int (* )[5]";
注意(typedef 与 宏定义的区别):
int main()
{
int p1,p2,p3,p4;
typedef int* ptr_t;
#define PTR_T int*;
ptr_t p1,p2; // p1,p2均为指针变量;
PTR_T p3,p4; // p3是指针变量,p4是整型变量。<替换后:int* p3,p4;>
}
17.函数指针数组(多用于回调函数)
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组, 比如:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是:parr1
parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢?
是 int (* )( ) 类型的函数指针 。