指针进阶:
通过指针基础我们已经了解了指针,这篇文章我们会举大量的例子,使我们对指针透彻理解,我们下来看一段代码:
int main()
{
char a[] = "ab";
char* pc = a;
printf("%c\n", *pc);
printf("%s\n", pc);
//当指针接收的是字符串时,打印这个指针所指向的字符串不需要解引用
return 0;
}
当指针接收的是字符串时,打印这个指针所指向的字符串不需要解引用;可是当要打印首个元素时,需要解引用。
int main()
{
char* ch = "abcdef";
//"abcdef"是一个常量字符串
printf("%c\n", *ch);
printf("%s\n", ch);
return 0;
}
在这个例子中,字符串“abcdef”,储存在连续的无名储存区中,通过语句ch=“abcdef”,将无名储存区的首地址赋给指针ch,也就是说:指针变量ch指向无名储存区域的首地址,而不是把无名储存区域的内容保存在ch中,因为它们各自所占空间大小不同(指针变量大小是固定的)。
道理同上,打印字符串不需要解引用,打印首元素需要解引用。
此时我们想修改其中一个字符。
int main()
{
char* ch = "abcdef";
//"abcdef"是一个常量字符串
//所以*p就是首个元素
*ch= 'W';
printf("%s\n", ch);
return 0;
}
无法输出结果,因为"abcdef"是常量字符串,常量意味着不可改变,这就是错误的,所以最好这样写const char*ch,在星号前加上const,下面就直接报错了。
我们也可以这样访问:
指针的比较:
在基础篇我们其实就已经已经讲过指针的比较了(详情指针(基础篇)-CSDN博客),指针比较必须是指向同一块内存的,我们来看代码。
int main()
{
char arr1[] = "abcd";
char arr2[] = "abcd";
char* p1 = "abcd";
char* p2 = "abcd";
if (arr1 == arr2)
//比较的是地址
printf("hehe\n");
else
printf("haha\n");
return 0;
}
创建了两个字符型数组,虽然内容一样,可是两个数组各有各的内存空间,地址完全不同,所以执行else。
但是p1和p2确实指向同一块空间,这就很奇怪,内容相同的常量字符串只会保存一份,所以他们只想的空间的地址相同。
还有一种指针类型是空指针类型,不是整形,不是字符型,不是数组型,而是一种空指针类型。空指针类型可以接收任何类型的地址。
int main()
{
int a = 10;
void* p = &a;//空指针可以接收任意类型的地址
//void* 类型指针不能进行解引用操作
p++;//void* 类型的指针不能进行+-操作(不知道步长)
printf("%p\n", p);//空指针有地址
printf("%d\n", *p);//空指针类型不能通过解引用来找到原有数据
return 0;
}
此时会报错,是编译性错误。
当const修饰变量时,我们不能直接去修改它,但是还是可以通过指针修改。
const分为放在*的左边和右边,控制的权限不一样。
当constant限制的是*p,意思是不能通过p来修改指向的空间内容,就是不能解引用了;当const放在*的右边,限制的是p变量,也就是p变量不能被修改了,没办法再指向其他地址了,但*p不受限制,还是可以通过p来修改所指向的变量。
指针练习题:
此时我们已经几乎会所有的指针相关概念,那么我们就拿一些小习题练练手。
习题一:
int main()
{
int a[5] = { 1,2,3,4,5 };
int* ptr = (int*)(&a + 1);//下一个数组的地址,就变成了数组指针类型,之后强制转换
printf("%d,%d\n", *(a + 1), *(ptr - 1));//-1就是数组的最后一个元素
return 0;
}
把&a+1的地址强制付给ptr,因为取地址a+1是下一个数组的地址,所以要强制转换类型。注意:*(a+1)==a[1]。
习题二:
struct Test
{
int Num;
char* pcNum;//64位指针大小是8字节
short sDate;//short是两个字节
char cha[2];
short sBa[4];
}*p;//此结构体大小为32
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;
}
习题三:
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 main()
{
int a[3][2] = { (0,1),(2,3),(4,5) };//注意是逗号运算符
int* p;
p = a[0];
printf("%d\n", p[0]);
printf("%d\n", p[1]);//等价于*(p+1)
//结果为1 3
return 0;
}
注意是小括号,小括号里逗号是运算符。
习题五:
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;
}
习题六:
int main()
{
int a[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)(*(a + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2));
//*(a+1)是第二行数组的地址,解引用后只想第二行数组的第一个元素
return 0;
}
二维数组如arr[1],是第一行第一个元素的地址,arr[2]是第二行第一个元素的地址。
习题七:
int main()
{
char* a[] = { "work","at","alibaba" };//指针数组,数组每一个元素是char*
//每个char*都存放每个元素首元素地址
char** pa = a;//二级指针,类型为char*,变量为指向char*
pa++;
printf("%s\n", *pa);
return 0;
}
习题八:
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[]={ c + 3,c + 2,c + 1,c };
//因为c是首元素地址,c+3指向第四个元素
//所以cp首元素指向c的第四个元素地址
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- * ++cpp + 3);
//注意此时解引用后又--了,原来c+1变为c也被保留下来
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
printf("%s\n", **cpp);
return 0;
}
看一下内存布局:
注意指针的自增自减,会改变原来指针指向。
习题九:
int main()
{
unsigned long pu[] = { 6,7,8,9,10 };
unsigned long* pulptr;
pulptr = pu;
*(pulptr + 3) += 3;//指针指向没有改变
//把第4个元素的值换为12
printf("%d,%d\n", *pulptr, *(pulptr + 3));
return 0;
}
指针sizeof和strlen习题:
先看几个知识点,sizeof读取‘\0’,strlen不读取‘\0’;strlen接收的是地址。一定要记得,字符串的双引号中最后包含一个'/0',可以不写,但是有。如果用大括号是字符串,没有声明个数,也没有声明'/0',也不知道字符串在哪里结束;若声明了个数,一定要有'/0'的位置。
两个例外情况:
- sizeof(数组名) - 数组名表示整个数组大小。
- &数组名 - 数组名表示整个数组。
习题一:
int main()
{
//数组名是元素地址
//例外:
//1.sizeof(数组名) - 数组名表示整个数组
//2.&数组名 - 数组名表示整个数组
int a[] = { 1,2,3,4 };//4*4=16
printf("%d\n", sizeof(a)); //16 sizeof(数组名)-计算数组总大小
printf("%d\n", sizeof(a+0));//8/4 数组名+0,这里表示首元素地址,地址大小就是指针大小
printf("%d\n", sizeof(*a)); //4 数组名解引用,找到数据类型
printf("%d\n", sizeof(a+1));//8/4 指针指向数组名第二个元素,是指针大小
printf("%d\n", sizeof(a[1]));//4 数组第二个元素大小
printf("%d\n", sizeof(&a)); //8/4 &a是数组地址,大小为指针大小
printf("%d\n", sizeof(*&a));//16 &a使整个数组的地址,解引用后就是整个数组大小
printf("%d\n", sizeof(&a + 1));//8/4 先&a整个数组大小,之后指向下一个数组,还是地址
printf("%d\n", sizeof(&a[0]));//8/4 []方块优先级高,还是地址
printf("%d\n", sizeof(&a[0]+1));//8/4 第二个元素的地址,还是地址
return 0;
}
习题二:
int main()
{
char a[] = { 'a','b','c','d','e','f' };
//strlen函数也是从首元素地址读取到'\0'为止
printf("%d\n", strlen(a)); //随机值,strlen找到'\0'才停止
printf("%d\n", strlen(a+0)); //随机值,和上一个一样
//printf("%d\n", strlen(*a)); //随机值,'a' - 97 strlen从地址97开始寻找 报错
//printf("%d\n", strlen(a[1])); //和上一个一样
printf("%d\n", strlen(&a)); //和第一个一样
printf("%d\n", strlen(&a+1)); //和第一个相差6
printf("%d\n", strlen(&a[0]+1));//和第一个相差1
return 0;
}
strlen函数读取到‘\0’停止,在大括号中没有‘\0’,所以第1个是随机数。
习题三:
用指针定义一个字符串并研究大小。
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p)); //8/4 p是指针,存放a的地址
printf("%d\n", sizeof(p+1)); //8/4 b的地址,还是地址
printf("%d\n", sizeof(*p)); //1 是数据类型的大小
//解引用后找到a,a是字符型大小为1
printf("%d\n", sizeof(p[0])); //1 是数据类型大小
printf("%d\n", sizeof(&p)); //8/4 还是地址
printf("%d\n", sizeof(&p+1)); //8/4
printf("%d\n", sizeof(&p[0]+1));//8/4
return 0;
}
习题四:
int main()
{
char a[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(a)); //6 这里不是字符串,所以是6个字节
printf("%d\n", sizeof(a+0)); //4/8 还是地址
printf("%d\n", sizeof(*a)); //1 解引用后就是数据类型大小
printf("%d\n", sizeof(a[1]));//1 数组第一个元素大小
printf("%d\n", sizeof(&a)); //4/8 地址
printf("%d\n", sizeof(&a+1));//4/8 地址
printf("%d\n", sizeof(&a[0]+1));// 地址
return 0;
}
习题五:
int main()
{
char a[] = "abcdef";
printf("%d\n", sizeof(a)); //7 因为有默认'\0',sizeof读取它
printf("%d\n", sizeof(a+0)); //8 地址
printf("%d\n", sizeof(*a)); //1 数据类型大小
printf("%d\n", sizeof(a[1])); //1
printf("%d\n", sizeof(&a)); //8 地址
printf("%d\n", sizeof(&a+1)); //8
printf("%d\n", sizeof(&a[0]+1));//8
return 0;
}
习题六:
int main()
{
char a[] = "abcdef";
printf("%d\n", strlen(a)); //6 strlen不读取'\0'
printf("%d\n", strlen(a+0)); //6 首元素地址
//printf("%d\n", strlen(*a)); //报错 非法访问
//printf("%d\n", strlen(a[1])); //报错 非法访问
printf("%d\n", strlen(&a)); //6 有警告,这相当于数组,应该用数组指针接收
printf("%d\n", strlen(&a+1)); //随机值
printf("%d\n", strlen(&a[0]+1));//5 从b开始读取
return 0;
}
习题七:
int main()
{
char* p = "abcdef";
printf("%d\n", sizeof(p)); //8/4 p是指针,存放a的地址
printf("%d\n", sizeof(p+1)); //8/4 b的地址,还是地址
printf("%d\n", sizeof(*p)); //1 是数据类型的大小
//解引用后找到a,a是字符型大小为1
printf("%d\n", sizeof(p[0])); //1 是数据类型大小
printf("%d\n", sizeof(&p)); //8/4 还是地址
printf("%d\n", sizeof(&p+1)); //8/4
printf("%d\n", sizeof(&p[0]+1));//8/4
return 0;
}
习题八:
int main()
{
char* p = "abcdef";
printf("%d\n", strlen(p)); //6 a的地址被接收,之后计算长度
printf("%d\n", strlen(p + 1)); //5 b的地址,长度少了1
//printf("%d\n", strlen(*p)); //报错
// a的ASCII码是97,从地址97处读取,报错
//printf("%d\n", strlen(p[0])); //报错
printf("%d\n", strlen(&p)); //随机值
printf("%d\n", strlen(&p+ 1)); //随机值
printf("%d\n", strlen(&p[0] + 1));//5 第二个元素读取地址
return 0;
}
习题八:
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));
//48 总数组大小
printf("%d\n", sizeof(a[0][0]));
//4 第一个数组第一个元素的大小
printf("%d\n", sizeof(a[0]));
//16 第一个元素是一维数组,一维数组的大小
printf("%d\n", sizeof(a[0]+1));
//8/4 a[0]是第一行的数组名,数组名此时是首元素地址,a[0]是第一行第一个元素地址
//所以 a[0]+1就是第一行第二个元素地址,地址的大小
printf("%d\n", sizeof(*(a[0]+1)));
//4 第一个数组中第二个元素的地址,解引用后找到元素,是类型的大小
printf("%d\n", sizeof(a+1));
//8/4 第二个元素是一维数组的地址,地址的大小
printf("%d\n", sizeof(*(a+1)));
//16 sizeof(a[1])第二个数组中的第一个元素的地址,解引用后是它第二个元素的大小
printf("%d\n", sizeof(&a[0]+1));
//8/4 第二个一维数组的地址,地址的大小
printf("%d\n", sizeof(*(&a[0] + 1)));
//16 第二个一维数组地址解引用,第二个一维数组元素的大小
printf("%d\n", sizeof(*a));
//16 第一行一维数组元素的大小
printf("%d\n", sizeof(a[3]));
//16 sizeof不去真实计算,可能会举一反三,根据类型计算大小
return 0;
}
a是二维数组的数组名,没有sizeof(数组名),也没有&(数组名),所以a是首元素地址,二维数组的首元素是第一行的一维数组。对二维数组解引用,求大小是第一行的大小。
习题九:
这里可以看出指向常量字符串是指向字符串的首元素地址。