本文篇幅较长,建议移步到电脑端进行观看,接下来就是从从浅入深理解指针,希望大家可以耐心看完,学有所成!
在讲内存和地址之前,我们想有个生活中的案例:
假设有一栋宿舍楼,把你放在楼里,楼上有100个房间,但是房间没有编号,你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
如果把上面的例子对照到计算中,又是怎么样呢?
其实也是把内存划分为一个个的内存单元,每个内存单元的大小取1个字节。
bit - 比特位
byte - 字节
KB
MB
GB
TB
PB
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间,比如:
int main()
{
int a = 10;
return 0;
}
0x005FFC54 0a
0x005FFC55 00
0x005FFC56 00
0x005FFC57 00
那我们如何能得到a的地址呢?
#include <stdio.h>
int main() {
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
按照我画图的例子,会打印处理:005FFC54
那我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量
中。
比如:
int main() {
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
int*
,我们该如何理解指针的类型呢?int a = 10;
int* pa = &a;
int*
,*
是在说明pa是指针变量,而前面的int
是在说明pa指向的是整型(int)类型的对象。char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?
char ch = 'w';
char* pc = &ch;
我们将地址保存起来,未来是要使用的,那怎么使用呢?
在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品。
C语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符(*)。
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
int main()
{
int num = 10;
int* p = #
char ch = 'w';
char* pc = &ch;
printf("%zd\n", sizeof(p));
printf("%zd\n", sizeof(pc));
return 0;
}
我们这里是x86环境下,猜猜这里这个p
和pc
的大小是多少?p是4个字节?pc是1个字节?
让我们来看看~~
可以看到,都是4个字节,指针变量的大小是固定的,不要以为char*
类型的就小,看不起char*
类型的指针~~
x86环境下为什么
char*
的指针变量和int*
的指针变量都是4个字节呢?
指针变量是干什么呢?是为了存放地址的
那指针变量的大小是取决于存放一个地址需要多大的空间!!!
地址都是32个0/1组成的二进制序列的话,那么存放这个地址需要的空间的大小就是4个字节,所以指针变量的大小都是4个字节~~
同样x64环境,64根地址线,地址就是64个0/1组成的二进制序列,存放这样的地址,需要8个字节,所以指针变量的大小就是8个字节~~
如果不相信,我们在VS中试一下~~
#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;
}
指针
看扁了~~结论:
指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习~~
代码一:
#include <stdio.h>
int main()
{
int n = 0x11223344;
int* pi = &n;
*pi = 0;
return 0;
}
代码二:
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = &n;
*pc = 0;
return 0;
}
F10
,如果有的同学是笔记本,就在笔记本上按Fn+F10
,开始调试大小端存储
如果还有同学不知道的话可以看看这个章节->C生万物 | 深度挖掘数据在计算机内部的存储现在再来看第二个代码~~
char*
类型的变量里,那能不能放的下?*pc = 0
,我们这个是修改的几个字节?char*
的指针变量结论:
char*
类型的指针解引用访问1个字节,int*
类型的指针一次访问4个字节#include <stdio.h>
int main()
{
int n = 0x11223344;
int* p = &n;
char* pc = &n;
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1);
printf("pc = %p\n", pc);
printf("pc + 1 = %p\n", pc + 1);
return 0;
}
char*
类型的指针变量+1
跳过1个字节, int*
类型的指针变量+1
跳过了4个字节。结论:
那有的同学会问,指针类型这些特点,怎么是使用呢?
我们先来回忆一下数组的方式~~
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//下标的方式
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
return 0;
}
我们再用指针的方式来访问~~
for
循环中*p
找到arr每个的元素,那找到一个元素,还想找下一个元素怎么办?那就要加1,因为p是整形指针,+1
跳过4个字节,正好找到下一个元素int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//下标的方式
int sz = sizeof(arr) / sizeof(arr[0]);
//int i = 0;
//for (i = 0; i < sz; i++) {
// printf("%d ", arr[i]);
//}
//指针的方式
int i = 0;
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d ", *p);
p = p + 1;
}
return 0;
}
p+1
,我直接*(p+i)
,这样可以吗?当然可以!!!int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//指针的方式
int i = 0;
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d ", *(p + i));
}
return 0;
}
相信看完了上面,你对指针类型有一个对应理解~~
#include<stdio.h>
int main() {
int n = 100;
n = 200;
printf("%d\n", n);
return 0;
}
int main() {
const int n = 100;
n = 200;//err
printf("%d\n", n);
return 0;
}
int main() {
const int n = 100;
int* p = &n;
*p = 200;
printf("%d\n", n);
return 0;
}
n
要被const
修饰呢?就是为了不能被修改,如果p
拿到n
的地址就能修改n
,这样就打破了const
的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?const修饰指针有两种情况:
*
的左边*
的右边const
放在*
的左边~~int main() {
int m = 100;
int n = 10;
const int* p = &n;
*p = 0;
p = &m;
printf("%d\n", n);
return 0;
}
p
指向的值不能被修改const
放在*
的左边~~int main() {
int m = 100;
int n = 10;
int* const p = &n;
*p = 0;
p = &m;
printf("%d\n", n);
return 0;
}
结论:
cons
t如果放在*
的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。const
如果放在*
的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。int const * p = &n;
int *const p = &n;
const
放在*
左边还是右边int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
#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+i
就访问到每一个元素了~~int main() {
int arr[10] = { 0 };
int ret = &arr[9] - &arr[0];
printf("%d", ret);
return 0;
}
也可以这样理解,&arr[0]+9
—>>>&arr[9]
结论: 指针-指针得到的绝对值,是指针和指针之间元素的个数
strlen
吗?strlen
的功能是求字符串长度,如果有同学不了解这个函数的话可以去cplusplus网站上看一下int main()
{
char arr[] = "abcdef";
int len = strlen(arr);
printf("%d\n", len);
return 0;
}
\0
,让我求长度,我就统计\0
之前出现字符的个数my_strlen
,我们把数组传参,然后形参以指针接收,指针指向了数组首元素的地址,然后我们定义个计数器count,如果p!=\0
,count++
,p++
,最后返回count的个数~~int my_strlen(char* p)
{
int count = 0;
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
\0
为止,最后返回p-s就得到了元素的个数~~#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;
}
明白了上面的内容,我们再来将一个指针的关系运算~~
#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;
}
指针未初始化:
#include <stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
cccccccc
不能!!!
指针越界访问:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
我们这里的p++
是先执行的,而p++
是先使用后++
我们这里可以看到,我把arr[0]
的地址放入了*p
指针变量,然后我们进行遍历赋值,那我们这里判断条件是不是就越界访问了,超出了数组的范围,当指针指向的范围超出数组arr的范围时,p就是野指针,这是很危险的~~
我们这里还可以调试一下看看~~
arr
造到了破坏,而越界访问的内容也修改了,如此可见,这多么的危险!!!#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
test()
,返回一个地址,既然这个函数返回的是一个地址,那我用一个指针来接收,然后*p
这个地址n
在出这个函数的时候被销毁了,这就会造成指针指向的空间释放~~NULL
什么意思呢,我们用代码来说~~
int main()
{
int a = 10;
int* p = &a;
return 0;
}
ptr
,我现在不用,但我可以在后面才会用,但是这个指针变量不能空着,这个时候我们要给他初始化NULL
int main()
{
int a = 10;
int* p = &a;
int* ptr = NULL;
return 0;
}
那有的同学会说,那我直接给他赋值为0,可不可以呢?本质上是可以的,但是,当我们赋值为0了,有的时候会以为是一个整数,而我们赋值为NULL
的时候,那就很明显了,一看就是空~~
NULL
是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
这里我们也可以看到~~
NULL
NULL
int main()
{
int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
int* p = &arr[0];
for (int i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用的时候,判断p不为NULL的时候再使用
//...
p = &arr[0];//重新让p获得地址
if (p != NULL) //判断
{
//...
}
return 0;
}
test
函数,函数里创建了个数组,数组是局部变量,而我们返回了这个数组的地址,我们使用了一个指针变量p
来接收,当test
函数返回的时候局部变量已经被操作系统回收了,这就会造成野指针,如果有看过函数的栈帧的创建与销毁的话就明白了int* test()
{
//局部变量
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//.....
return arr;
}
int main()
{
int* p = test();//p就是野指针
return 0;
}
assert(p != NULL);
p
是否等于NULL
。如果确实不等于NULL ,程序继续运行,否则就会终止运行,并且给出报错信息提示。#include <assert.h>
int main()
{
int a = 10;
int* p = &a;
assert(p != NULL);
return 0;
}
#include <assert.h>
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);
return 0;
}
assert()
宏接受一个表达式作为参数。如果该表达式为真(返回值非零), assert()
不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert()
就会报错,在标准错误流stderr
中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。assert()
的使用对程序员是非常友好的,使用assert()
有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert() 的机制。如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>
语句的前面,定义一个宏NDEBUG
。#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁用文件中所有的assert()
语句。如果程序又出现问题,可以移除这条#define NDBUG
指令(或者把它注释掉),再次编译,这样就重新启用了assert()
语句。
assert()
的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在debug
中使用,在release
版本中选择禁用assert
就行,在VS这样的集成开发环境中,在release
版本中,直接就是优化掉了。这样在debug
版本写有利于程序员排查问题,在release
版本不影响用户使用时程序的效率。
一直叫传值调用,一种叫传址调用,接下来我们继续看~~
#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;
}
当我们运行代码,结果如下:
0x0039f9f0
,b的地址是0x0039f9e4
,在调用Swap1
函数时,将a和b传递给了Swap1
函数,在Swap1
函数内部创建了形参x和y接收a和b的值,但是x的地址是0x0039f90c
,y的地址是0x0039f910
,x和y确实接收到了a和b的值,不过x的地址和a的地址不一样,y的地址和b的地址不一样,相当于x和y是独立的空间,那么在Swap1
函数内部交换x和y的值,自然不会影响a和b,当Swap1
函数调用结束后回到main
函数,a和b的没法交换。Swap1
函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。结论: 实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。所以Swap是失败的了。
#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);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
首先看输出结果:
my_strlen()
*str
的指针来接收,我们这个函数期望这个字符串来修改吗?不期望,这里我们再加上一个const
str
,确保指针的有效性str
指向a
的,当str!='\0'
,str++
,计数器也++,最后返回计数器~~size_t
计数器方式
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str);
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
&arr[0]
的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试。#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
4
,那是不是呢?#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
&数组名, 这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
这时有好奇的同学,再试一下这个代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
数组的地址和数组首元素的地址的值是一模一样的,那它们有什么区别呢,接下来继续看~~
我们再来看下面这段代码~~
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr + 1);
return 0;
}
&arr
和 &arr+1
相差40个字节,这就是因为&arr
是数组的地址,+1
操作是跳过整个数组的。&arr[0]
它的类型是int*
,而&arr加一加了40个字节,它的类型是什么呢?我们这里留个悬念,后面都会将~~有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
//输入
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输入
int* p = arr;
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
我们定义了一个整型数组 arr 和一个指向该数组的指针 p,其中,表示数组元素的方法有两种,一种是 *(p + i),另一种是 arr[i]。
这个代码搞明白后,我们再试一下,如果我们再分析一下,数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
//输入
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输入
int* p = arr;
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
(p+i)
换成p[i]
也是能够正常打印的,所以本质上p[i]
是等价于 *(p+i)
。arr[i]
应该等价于*(arr+i)
,数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。arr[i]
== *(arr+i)
== *(i+arr)
== i[arr]
是不是也可以这样,照样也能访问~~int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++) {
printf("%p ======== %p\n", p + i, &arr[i]);
}
return 0;
}
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
void test1(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test2(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算一个指针变量的大小
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
test1(arr);
test2(arr);
return 0;
}
总结: 一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
接下来我们就学习一下这个冒泡排序,主要学习两个内容~~
- 学习冒泡排序
- 学习数组传参
int main() {
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//进行排序
return 0;
}
int main() {
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//进行排序
int sz = sizeof(arr) / sizeof(arr[0]);
sort(arr,sz);
return 0;
}
代码如下:
void sort(int arr[], int sz) {
//确定冒泡排序的趟数~~
int i = 0;
for (i = 0; i < sz - 1; i++) {
//一趟冒泡排序
int j = 0;
for (j = 0; j < sz - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
//交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
void print(int arr[], int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//进行排序
int sz = sizeof(arr) / sizeof(arr[0]);
sort(arr,sz);
print(arr, sz);
return 0;
}
int arr[] = { 9,0,1,2,3,4,5,6,7,8 };
void sort(int arr[], int sz) {
//确定冒泡排序的趟数~~
int i = 0;
for (i = 0; i < sz - 1; i++) {
//一趟冒泡排序
int j = 0;
int flag = 1;//假设数组是有序的
for (j = 0; j < sz - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
//交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;//不是有序
}
}
if (flag = 1) {
break;
}
}
}
#include<stdio.h>
int main() {
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
int*** pppa = &ppa
对于二级指针的运算有:
*ppa
通过对ppa
中的地址进行解引用,这样找到的是pa
, *ppa
其实访问的就是
int b = 20;
*ppa = &b;//等价于 pa = &b;
**ppa
先通过*ppa
找到pa
,然后对pa
进行解引用操作: *pa
,那找到的是a
.
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
我们类比一下:
整形数组和字符数组:
如下图:
比如:
int main() {
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
int* parr[5] = { &a,&b,&c,&d,&e };
return 0;
}
int main() {
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
int* parr[5] = { &a,&b,&c,&d,&e };
int i = 0;
for (i = 0; i < 5; i++) {
printf("%d ", *(parr[i]));
}
return 0;
}
parr[i]
找到了每个元素的地址,然后解引用,就找到了,便可以打印出来~~#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//数组名是数组首元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[3] = { arr1, arr2, arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
parr[i]
是访问parr数组的元素,parr[i]
找到的数组元素指向了整型一维数组,parr[i][j]
就是整型一维数组中的元素。在指针的类型中我们知道有一种指针类型为字符指针char*
;
我们这里定义了ch变量,里面存了个字符 w
然后我将这个变量的地址取出来放到pc里,它的类型是char*
,pc就是字符指针变量
int main()
{
char ch = 'w';
char* pc = &ch;
return 0;
}
char* p = "abcdefghi";
我们可以这样验证:
char* p = "abcdefghi";
printf("%c", *p);
const
来修饰const char* p = "abcdefghi";
那我想要打印一下这个字符串,怎么办?
我们就以%s
的方式来打印
const char* p = "abcdefghi";
printf("%s", p);
《剑指offer》中收录了一道和字符串相关的笔试题,我们一起来学习一下:
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
之前我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。
数组指针变量是指针变量?还是数组?
答案是:指针变量
我们已经熟悉:
整形指针变量: int * pint;
存放的是整形变量的地址,能够指向整形数据的指针。
浮点型指针变量: float * pf;
存放浮点型变量的地址,能够指向浮点型数据的指针。
数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
int *p1[10];
int (*p2)[10];
这两个是哪个呢?
数组指针变量
int (*p)[10];
解释: p先和*结合,说明p是一个指针变量变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫 数组指针。
这里要注意:[]
的优先级要高于*
号的,所以必须加上()
来保证p先和*结合。
&数组名
。int arr[10] = { 0 };
&arr;//得到的就是数组的地址
int(*p)[10] = &arr;
int arr[10] = { 0 };
arr; //数组首元素的地址 -- int*
&arr;//数组的地址 -- int(*)[10]
#include <stdio.h>
void test(int a[3][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 ", a[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
这里实参是二维数组,形参也写成二维数组的形式,那还有什么其他的写法吗?
首先我们再次理解一下二维数组,二维数组起始可以看做是每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是个一维数组.
如下图:
也可以这样理解:
二维数组的每一行是一个一维数组,这个一维数组可以看做是二维数组的第一个元素,所以二维数组也可以认为是一维数组的数组
那么二维数组的数组名表示数组首元素的地址,就是第一行的地址,也就是一个一维数组的地址
int [5]
,所以第一行的地址的类型就是数组指针类型int(*)[5]
。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:#include <stdio.h>
void test(int(*p)[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 ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
总结: 二维数组传参,形参的部分可以写成数组,也可以写成指针形式。
什么是函数指针变量呢?
那么函数是否有地址呢?
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;
int Add(int x, int y)
{
return x + y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
那我们是不是要进行使用,怎么使用呢?
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &Add;
int r = (*pf)(3, 5);//调用函数指针
printf("r = %d\n", r);
return 0;
}
*
也行,但是写上就更容易理解,可读性更高一些~~int r = pf(3, 5);
函数指针类型解析:
代码1:
(*(void (*)())0)();
代码2:
void (*signal(int , void(*)(int)))(int);
signal
是一个函数的函数名,上面的代码是一次函数声明,声明的signal
函数有两个参数,第一个参数是int类型的,第二个参数是函数指针类型的,该函数指针指向的函数参数是int类型,返回类型是void
signal
函数的返回类型也是一个函数指针,该函数指针指向的函数,参数是int,返回类型也是void两段代码均出自:《C陷阱和缺陷》这本书
typedef 是用来类型重命名的,可以将复杂的类型,简单化。
比如,你觉得unsigned int
写起来不方便,如果能写成uint
就方便多了,那么我们可以使用:
typedef unsigned int uint;
//将unsigned int 重命名为uint
int*
重命名为ptr_t
,这样写:typedef int* ptr_t;
int(*)[5]
,需要重命名为parr_t
,那可以这样写:typedef int(*parr_t)[5]; //新的类型名必须在*的右边
void(*)(int)
类型重命名为pf_t
,就可以这样写:typedef void(*pfun_t)(int);//新的类型名必须在*的右边
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
根据前面学的,我们再来类比一下:
那么我们是不是也可以将这个函数放到数组里?接下来我们来学习函数指针数组
比如:
int *arr[10];
//数组的每个元素是int*
int (*parr1[3])();
parr1
先和[]
结合,说明 parr1
是数组,数组的内容是什么呢?int (*)()
类型的函数指针。那么有用吗,有的!!,接下来就来到我们的转移表模块~~
函数指针数组的用途:转移表
初阶版本:
#include <stdio.h>
void menu()
{
printf("*************************\n");
printf("********1:add 2:sub****** \n");
printf("********3:mul 4:div******\n");
printf("********0:exit ******\n");
printf("*************************\n");
}
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 x, y;
int input = 1;
int ret = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
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;
}
改进版本:
#include <stdio.h>
void menu()
{
printf("*************************\n");
printf("********1:add 2:sub****** \n");
printf("********3:mul 4:div******\n");
printf("********0:exit ******\n");
printf("*************************\n");
}
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 x, y;
int input = 1;
int ret = 0;
int(*pfArr[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*pfArr[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入有误,请重新选择\n");
}
} while (input);
return 0;
}
如何定义?
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
函数指针数组
的指针也不怎么重要,这里就不细讲了,让大家知道有这个概念,有兴趣的同学可以自行查阅~~通过函数指针调用的函数
。不是
由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的
,用于对该事件或条件进行响应。我们首先对上面的计算器来进行举例:
#include <stdio.h>
menu()
{
printf("*************************\n");
printf("********1:add 2:sub****** \n");
printf("********3:mul 4:div******\n");
printf("********0:exit ******\n");
printf("*************************\n");
}
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, x, y;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*pf)(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do
{
menu();
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;
}
图解:
就好像是老板把任务分配给组长,组长再分配给组员实现内容~~
这个qsort函数有4个参数:
void qsort(void* base,
size_t num,
size_t size,
int (*compar)(const void*, const void*));
这里有个void*的指针类型,这个指针类型是通用指针类型
,这个指针类型可以接收任意类型数据的地址,就可以理解成指针垃圾桶,谁的地址都可以往里扔~~
是一个无具体类型的指针,所以就不能+1,也不能解引用
我们也介绍完了这个函数,我们来使用排序一下整形数组
写出我们的主函数:
int main()
{
int arr[] = { 3,2,5,6,8,7,9,1,4 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
for (size_t i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
那么对于整形数据也可以排序了,是不是也可以对字符/字符串类型排序,对结构体也可以进行排序
首先我们对于名字进行排序,对于名字排序,名字是个字符串,不能使用大小于号来进行比较,我们需要用到strcmp函数来进行比较,打开cplusplus网站搜索stcmp,发现这个函数的返回值也是整形,和qsort基本一样,我们使用的时候可以直接return
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu s[] = { {"zhangsan",20 },{"lisi",18},{"wangwu",30} };
qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), stu_cmp_by_name);
for (int i = 0; i < sizeof(s) / sizeof(s[0]); i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
return 0;
}
int stu_cmp_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name);
}
struct Stu
{
char name[20];
int age;
};
int stu_cmp_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int main()
{
struct Stu s[] = { {"zhangsan",20 },{"lisi",18},{"wangwu",30} };
qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), stu_cmp_by_age);
for (int i = 0; i < sizeof(s) / sizeof(s[0]); i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
return 0;
}
qsort可以排序整形数据/字符数据/结构体数据…
可以使用qsort函数对数据进行排序
使用回调函数,模拟实现qsort(采用冒泡的方式)。
今天我们使用冒泡排序,来实现一个对任意类型能够排序的函数
我们先来实现一个整形的冒泡排序,然后再进行改造~~
void bubble_sort(int arr[], int sz)
{
int i = 0;
for ( i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 3,2,5,6,8,7,9,1,4 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (size_t i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
接下来我们开始改造~~
e1是一个指针,存放了要比较元素的地址
e2是一个指针,存放了要比较元素的地址
e1指向的元素大于e2指向的元素,返回>0的数字
e1指向的元素等于e2指向的元素,返回0
e1指向的元素小于e2指向的元素,返回<0的数字
我们来看完整代码
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
void Swap(char* buf1, char* buf2, size_t size)
{
for (int i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
++buf1;
++buf2;
}
}
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void my_bubble_qsort(void* base, size_t num, size_t size, int(*cmp)(const void* e1, const void* e2))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - i - 1; j++)
{
// 返回了大于0的数就交换
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[] = { 3,2,5,6,8,7,9,1,4,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
my_bubble_qsort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
return 0;
}
下面图解可以看一下~~
sizeof
和strlen
在学习操作符的时候,我们学习了sizeof
, sizeof
计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
我们就来开始学习了解sizeof~~
size_t
其实专门是设计给sizeof
的,表示sizeof
的返回值类型sizeof
计算的不可能是负数吧,所以size_t
是为sizeof
来设计的~~列如:
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
return 0;
}
sizeof
只关注占用内存空间的大小,不在乎内存中存放什么数据,我们一会来详细看~~strlen 是C语言库函数,功能是求字符串长度。函数原型如下:
size_t strlen ( const char * str );
strlen
函数的参数str
中这个地址开始向后,\0
之前字符串中字符的个数。strlen
函数会一直向后找\0
字符,直到找到为止,所以可能存在越界查找。我们来看下面的代码:
int main()
{
char arr2[] = "abc";
printf("%d\n", strlen(arr2));
return 0;
}
3
\0
,strlen
是统计\0
之前的字符串的个数,结果是3
\0
会算出几呢?char arr2[] = "ab\0c";
printf("%d\n", strlen(arr2));
2
\0
它的结果是什么呢?char arr1[] = { 'a', 'b', 'c' };
printf("%d\n", strlen(arr1));
\0
。strlen
和sizeof
strlen:
sizeof:
string.h
\0
之前字符的个数\0
,如果没有\0
,就会持续往后找,可能会越界short s = 10;
int i = 2;
int n = sizeof(s = i + 4);
printf("%d\n", n);
printf("%d\n", s);
为什么是2和10呢?我们来分析一下~~
创建了一个短整型s,占两个字节,i是整形,占四个字节
这里的i+4得出的结果我要放到s类型,我一个4个整形的放到两个整形的空间,这要发生截断,截断之后就是s说了算,所以就是2个字节。
那么第二个,表达式放到sizeof内部不会真实计算的,不参与计算!!!所以原来的值就会打印什么值~~
如果还没有理解的话,我们来看一些笔试题,来加深一下印象~~
int main()
{
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));
return 0;
}
sizeof
内部单独放了一个数组名,数组名表示整个数组的大小,数组内有4个元素,每个元素4个字节,所以就是16
printf("%d\n", sizeof(a));
&
,所以a就是首元素的地址,是地址,大小就是4/8
个字节printf("%d\n", sizeof(a + 0));
*a == *(a+0) == a[0]
,*a 其实就是第一个元素,也就是a[0]
,大小就是4
个字节printf("%d\n", sizeof(*a));
4/8
printf("%d\n", sizeof(a + 1));
4
printf("%zd\n", sizeof(a[1]));
&a
取出的是数组的地址,但是数组的地址也是地址,是地址大小就是4 / 8
个字节printf("%zd\n", sizeof(&a));
int(*p)[4] = &a
,*p访问一个数组的大小,p+1就是跳过一个数组的大小,结果是16
printf("%d\n", sizeof(*&a));
4/8
printf("%zd\n", sizeof(&a + 1));
4/8
printf("%zd\n", sizeof(&a[0]));
4/8
printf("%zd\n", sizeof(&a[0] + 1));
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
return 0;
}
6
printf("%d\n", sizeof(arr));
4/8
个字节printf("%d\n", sizeof(arr + 0));
1
个字节printf("%d\n", sizeof(*arr));
1
个字节printf("%d\n", sizeof(arr[1]));
4/8
printf("%d\n", sizeof(&arr));
4/8
printf("%d\n", sizeof(&arr + 1));
4/8
printf("%d\n", sizeof(&arr[0] + 1));
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
\0
之前的元素个数所以就是随机值
printf("%d\n", strlen(arr));
随机值
~~printf("%d\n", strlen(arr+0));
a
,ASCLL码值是97,97传给strlen,会把97当成个地址,会非法访问,结果会报错
printf("%d\n", strlen(*arr));
报错
printf("%d\n", strlen(arr[1]));
随机值
printf("%d\n", strlen(&arr));
随机值
printf("%d\n", strlen(&arr+1));
随机值
printf("%d\n", strlen(&arr[0]+1));
abcdef\0
,这里面有\0~~char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
7
printf("%d\n", sizeof(arr));
arr + 0
还是首元素的地址,大小就是4/8
个字节printf("%d\n", sizeof(arr+0));
1
字节printf("%d\n", sizeof(*arr));
1
字节printf("%d\n", sizeof(arr[1]));
4/8
个字节printf("%d\n", sizeof(&arr));
4/8
个字节printf("%d\n", sizeof(&arr+1));
4/8
个字节printf("%d\n", sizeof(&arr[0]+1));
sizeof
换成strlen
~~char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
6
printf("%d\n", strlen(arr));
6
printf("%d\n", strlen(arr+0));
报错
,会非法访问printf("%d\n", strlen(*arr));
非法访问
~~printf("%d\n", strlen(arr[1]));
6
printf("%d\n", strlen(&arr));
随机值
printf("%d\n", strlen(&arr+1));
5
printf("%d\n", strlen(&arr[0]+1));
p
存放的是这个字符串a的地址char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
4/8
个字节printf("%d\n", sizeof(p));
4/8
个字节printf("%d\n", sizeof(p+1));
*p
是首字符,大小是1
字节printf("%d\n", sizeof(*p));
1
字节printf("%d\n", sizeof(p[0]));
4/8
个字节printf("%d\n", sizeof(&p));
4/8
个字节printf("%d\n", sizeof(&p+1));
4/8
个字节printf("%d\n", sizeof(&p[0]+1));
32位下:
64位下:
我们再来换成strlen:
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
6
printf("%d\n", strlen(p));
char*
,+1
跳过的就是一个char类型的数据,所以就来到了字符'b'
的地址处,向后找\0的话就最后的结果即为 5
printf("%d\n", strlen(p+1));
*p
取到的就是字符'a'
,strlen就会把字符a的ascll码值当地址传过去了,会产生非法访问,结果是err
printf("%d\n", strlen(*p));
*p == *(p+0) == p[0]
printf("%d\n", strlen(p[0]));
char*
从p所占空间的起始位置开始查找的,它不知道什么时候会遇到\0
,所以就会是随机值
printf("%d\n", strlen(&p));
&
取地址后它的类型就变成了char**
,+1
会跳过一个char*
类型的数据,它指向了字符串末尾的这个位置,从这里向后去进行找\0
,也是不知道什么时候会遇到,所以最后的结果还是随机值
printf("%d\n", strlen(&p+1));
'b'
,结果也就是5
printf("%d\n", strlen(&p[0]+1));
最后我们再来看二维数组,也是比较难的一部分,这里一定要认真看~~
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
48
printf("%d\n", sizeof(a));
4
个字节printf("%d\n", sizeof(a[0][0]));
a[0]
为第一行的数组名,而且它是单独放在sizeof()内部的,计算的是第一行这一整行的大小,里面有4个元素,每个元素都是4个字节,那么结果即为16
printf("%d\n", sizeof(a[0]));
a[0][0]
的地址,a[0]+1
是第一行第二个元素a[0][1]
的地址,是地址的大小是4/8
个字节printf("%d\n", sizeof(a[0] + 1));
a[0] + 1
是第一行第二个元素a[0][1]
的地址,*(a[0] + 1)
就是第一行第二个元素,大小是4
个字节printf("%d\n", sizeof(*(a[0] + 1)));
a
没有单独放在sizeof内部,没有&,数组名a就是数组首元素的地址,也就是第一行的地址,a+1
,就是第二行的地址,也就是等价于a -- int(*)[4]
---->a+1 -- int(*)[4]
,是地址就是4/8
printf("%d\n", sizeof(a + 1));
16
printf("%d\n", sizeof(*(a + 1)));
*(a + 1)
,计算的是第二行的元素大小,结果是16
printf("%d\n", sizeof(a[1]));
+1
的话也会跳过整个数组,此时也就来到了第二行,那么取到的便是第二行的地址,地址的大小即为 4/8
个字节printf("%d\n", sizeof(&a[0] + 1));
16
printf("%d\n", sizeof(*(&a[0] + 1)));
*a
就是一行的 等价于*(a+0) == a[0]
,取到的就是第一行的元素,结果是16
printf("%d\n", sizeof(*a));
这个二维数组不是只有三行吗,第三行的数组名为a[2],那a[3]不是越界了吗?
下面这个a[3]来说,虽然看上去存在越界,sizeof()
并不关心你有没有越界,不会真实的访问,而是知道你的类型即可,a[3]便是二维数组的第四行,虽然没有第四行,但是类型是确定的,那么大小就是确定的,计算sizeof(数组名)计算的是整个数组的大小,结果便是16
printf("%d\n", sizeof(a[3]));
总结:
数组名的意义:
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?
*(a+1)
首先a是在数组的第一个元素,因为没有&,然后加1是跳过一个元素的大小,就来到了元素2
的位置*(ptr - 1)
ptr刚刚是到了5的后面,然后-1就指向了5,然后解引用就打印出5
//由于还没学习结构体,这里告知结构体的大小是20个字节
//x86环境下
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
首先我们要知道0x开头的数字是16进制的数字
p + 0x1
结构体指针+1是跳过一个结构体的大小20
,是0x100020
吗?不是,而是0x100014
,因为这个是16进制的
(unsigned long)p + 0x1
指针p强制类型转换成(unsigned long),整形+1是加1结果是0x100001
(unsigned int*)p + 0x1
指针p强制类型转换成(unsigned int*),整形指针+1跳过4个字节,结果是0x100004
最后我们需要以%p
的形式打印出来,那么打印的是8个16进制打印出来
00100014
00100001
00100004
我们开看一下结果:
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
(int *)(&a + 1)
,&a+1
是取出整个地址+1,因为这个是数组类型,然后强制类型转换成了(int*),赋值给了ptr1
ptr1[-1]
,这个就相当于*(ptr1 - 1)
,然后就指向了4
int
类型,然后加1,加的是内存中一个字节的位置,然后赋给了ptr2,ptr2是整形指针,向后访问4个字节,又因为是小端存储,打印是以%x
打印的,所以结果是2000000
我们来看一下结果:
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
p[0]
也就是*(p+0)
,+0就相当于没加,结果打印的就是1我们来看一下结果:
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
-4
-4
以%p的方式打印,认为内存中存储的补码就是地址 ,结果是FFFFFFFC
我们来验证一下结果:
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
ptr1
,然后我们看ptr1-1,就是减去了一个整形,然后解引用就找到了10
a[1]
,指向了6,这里的强制类型转换是迷惑,不会发生什么作用的,赋值给了ptr2我们来验证一下结果:
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
at
我们来验证一下结果:
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}
我们来看一下下面的图片:
printf("%s\n", **++cpp);
POINT
再来看第二句:
printf("%s\n", *--*++cpp+3);
--
,也就是c + 1 - 1 = c,这个时候里面就不再是c + 1
这块地址,而是c这块地址,再对其进行解引用,到了E所在的地址,最后再 + 3即向后偏移3个字节也就是3个字符,就是E所在的位置,使用%s进行打印,便打印出了后面的ER
再来看第三句:
printf("%s\n", *cpp[-2]+3);
* *(cpp - 2) + 3
,也就是cpp向前偏移2个char**
的位置,就找到了cp这行地址中所存放的值为c + 3,这个地址保存着c这一行所在的地址,再解引用,就取到了F的地址,然后+3,打印出ST
最后来看最后一句:
printf("%s\n", cpp[-1][-1]+1);
*(*(cpp - 1) - 1) + 1
,首先就是将cpp向前偏移一个char**
的位置,然后解引用取到了第二行的内容c+2,然后再-1,为c+1,再解引用,就拿到了c+1中存放的内容,找到了N,然后最后面还有个+1,就找到了E,最后打印,结果位EW
好了,指针的所有内容就到这里就结束了~~
如果有什么问题可以私信我或者评论里交流~~
感谢大家的收看,希望我的文章可以帮助到正在阅读的你🌹🌹🌹