1.指针
1.1指针的概念和相关操作
地址:内存中每个字节都有一个编号,这个编号就叫指针,也叫地址
指针变量:专门用来存储这个地址编号的变量
指针 --->> 指针变量
地址 --->> 地址编号
#include<stdio.h>
int main(int argc, const char *argv[])
{
int a = 10; //程序在执行到定义变量的语句时,操作性系统的会根据变量的类型给变量分配内存空间
//通过变量名,可以直接访问到系统给变量a分配的内存
a = 20; //(写操作)
printf("a = %d\n",a); //20(读操作)//可以通过&(取地址符)获取变量地址(printf函数使用%p作为打印地址的占位符)
printf("&a = %p\n",&a); //终端输出一个16进制的数//指针变量可以保存变量的地址
int *p = &a; //定义指针变量的格式: 存储类型 数据类型 *指针变量名
printf("&a = %p\n",p);return 0;
}//&a 和 *p的值是相同的,都是变量a的地址
&:取地址符,可以用来获取变量的地址。
对于多字节的变量,取地址时取到的是该变量所占的存储区域中的第一个单元的地址,也就是 编号最小的那个,---首地址
* :在定义指针变量时,起到一个标识作用,标识定义是一个指针变量
在其他场景下,对指针变量取*操作时,表示操作指针保存地址里面的内容
变量和指针的关系
#include<stdio.h>
int main(int argc, const char *argv[])
{
int a = 10; //程序在执行到定义变量的语句时,操作性系统的会根据变量的类型给变量分配 内存空间
int *p1 = &a; //定义指针变量的格式: 存储类型 数据类型 *指针变量名
//指针p保存变量a的地址,也就是指针p指向变量。当指针指向变量后,就可以通过指针操作指向内存空间的内容
*p1 = 100;
printf("a = %d *p1 = %d\n", a, *p1); //a= 100 *p1=100
long b = &a;
printf("b = %#lx\n", b); //普通变量可以保存地址,编译不会报错
// *b = 1000; // 但是普通变量不能取*操作,不能操作指向内存空间的内容,编译会报错#if 0
//指针只能保存操作系统分配后内存空间的地址
int *p2 = 0x12345678;
printf("p2 = %p\n", p2);
*p2 = 1000; //这个操作的结果不可预知,因为操作系统不一定把0x1234678分给此操作的 内存
#endifreturn 0;
}
一行中可以多个指针变量,正确写法如下:
#include<stdio.h>
int main(int argc, const char *argv[])
{
int a = 10;int *p1, *p2;
p1 = &a;
p2 = &a;return 0;
}
错误写法: int *p1,p2 这种写法p1是指针变量,p2是普通的int变量
1.2野指针和空指针
野指针:定义指针时,如果没有初始化,里面存的就是随机地址。
对野指针取 * 操作的结果时不、可预知的的。
空指针:定义指针时,可以使用NULL来初始化。
对空指针取 * 操作的结果一定是段错误。
#define NULL((void *)0)
#include<stdio.h>
int main(int argc, const char *argv[])
{
int a = 10;int *p1, *p2;
p1 = &a;
p2 = &a;
int *p3 ; //野指针
int *p4 = NULL; //空指针
return 0;
}
2.指针变量的大小
64位系统中 指针都是8字节的
32位系统中 指针都是4字节的
#include <stdio.h>
int main(int argc, const char *argv[])
{
char *p1;
short *p2;
int *p3;
long *p4;
long long *p5;
float *p6;
double *p7;
printf("sizeof(char *) = %ld sizeof(p1) = %ld\n", sizeof(char *), sizeof(p1));
printf("sizeof(short *) = %ld sizeof(p2) = %ld\n", sizeof(short *), sizeof(p2));
printf("sizeof(int *) = %ld sizeof(p3) = %ld\n", sizeof(int *), sizeof(p3));
printf("sizeof(long *) = %ld sizeof(p4) = %ld\n", sizeof(long *), sizeof(p4));
printf("sizeof(long long *) = %ld sizeof(p5) = %ld\n", sizeof(long long *), sizeof(p5));
printf("sizeof(float *) = %ld sizeof(p6) = %ld\n", sizeof(float *), sizeof(p6));
printf("sizeof(double *) = %ld sizeof(p7) = %ld\n", sizeof(double *), sizeof(p7));
return 0;
}
3.指针的运算
指针里面存放的是地址,所以指针的运算,本质就是指针中保存的地址量来参与运算。相同类型的指针变量间做运算才有意义。
指针常用的运算:
算数运算: + - ++ --
关系运算: > < == !=
赋值运算: =
指针 + 整数n :表示加上n个操作空间的大小
#include<stdio.h>
int main(int argc, const char *argv[])
{
int arr[5] = {1,2,3,4,5};
int *p1 = &arr[0];
int *p2 = p1+2;
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);
// p1和p2相差8 表示8个字节 2个int
char *p3 =(char *)&arr[0]; //(char *)是把int类型的数组显式强转
char *p4 = p3+2;
printf("p3 = %p\n", p3);
printf("p4 = %p\n", p4);
// p1和p2相差2 表示2个字节 2个char:
return 0;
}
两个指做差: 结果是两个指针保存的地址相差的操作空间的个数
#include<stdio.h>
int main(int argc, const char *argv[])
{
int *p1 = &s[0];
int *p2 = &s[3];
printf("%ld\n", p2-p1); // 3 表示相差3个int
short *p3 = (short *)&s[0];
short *p4 = (short *)&s[3];
printf("%ld\n", p4-p3; // 6 表示相差6个short
return 0;
}
自增自减运算 以++ 为例 --同理
*++p 和 *p++ 的表达式结果不一样
#include<stdio.h>
int main(int argc, const char *argv[])
{
int arr[5] = {1,2,3,4,5};
int *p1 = &arr[0];
int r1 = *++p1;
printf("r1 = %d &s[1] = %p p1 = %p\n", r1, &arr[1], p1);//2 后面两个地址相同
int *p2 = &arr[0];
int r2 = *p2++;
printf("r2 = %d &s[1] = %p p2 = %p\n", r2, &arr[1], p2);//1 后面两个地址相同
int *p3 = &arr[0];
int r3 = (*p3)++; //相当于 int r3 = arr[0]++;
printf("r3 = %d &s[0] = %d s[1] = %d\n", r2, arr[0], arr[1]);//1 2 2
return 0;
}
指针的关系运算
#include<stdio.h>
int main(int argc, const char *argv[])
{
int arr[5] = {1,2,3,4,5};
int *p1 = &arr[0];
int r1 = *++p1;
printf("r1 = %d &s[1] = %p p1 = %p\n", r1, &arr[1], p1);//2 后面两个地址相同
int *p2 = &arr[0];
int r2 = *p2++;
printf("r2 = %d &s[1] = %p p2 = %p\n", r2, &arr[1], p2);//1 后面两个地址相同
//指针的关系运算
if(p2 == p1){ // 其他关系运算符 用法同理
printf("yes\n");
}else{
printf("no\n"); // 会输出no
}
return 0;
}
指针的赋值运算
指针变量也是变量 变量是可以被重新赋值的
#include<stdio.h>
int main(int argc, const char *argv[])
{
int arr[5] = {1,2,3,4,5};
int *p1 = &arr[0];
int *p2 = NULL;
p2 = p1; // 赋值
printf("*p2 = %d\n", *p2); // 1
return 0;
}
思考:x下面代码会输出什么?
int *p = NULL;
printf("%d %d %d\n", p+1, p, (p+1)-p);
输出:4 0 1
p 是地址为0的强转 NULL = (void *)0
p+1为4 是因为指针+1,表示加上1个i类型的大小4字节
(p+1)-p 为1 是因为 p+1和p都是指针,做差的结果就是两个指针保存的地址间相差的操作空间 的个数,相差1个int类型
4.大小端存储
不同的CPU和操作系统对多字节整型数据的存储方式是不一样的,分为大端存储和小端存储
大端存储:地址低位存储数据高位,地址高位存储数据地位
小端存储:地址低位存储数据高位,地址高位存储数据低位
如何使用简单的C语言程序,判断你所使用的主机是大端存储还是小端存储
#include <stdio.h>
int main(){
int a = 0x11223344;
#if 0
char *p = (char *)&a;
if(0x11 == *p){
printf("大端\n");
}else if(0x44 == *p){
printf("小端\n");
}
#else
if(0x11 == *((char *)&a)){
printf("大端\n");
}else if(0x44 == *((char *)&a)){
printf("小端\n");
}
#endif
return 0;
}
思考:在小端存储的主机上,下面的代码会输出什么?
int x = 0x44434241;
printf("%s\n", (char *)&x);
输出:ABCD后面是不确定的内容
5.指针和一维数组
数组名就是数组的首地址 操作空间是一个元素的大小。
int类型的两个数组元素的地址相差4字节
int main(int argc, const char *argv[])
{
int s[5] = {10, 20, 30, 40, 50};
printf("s = %p\n",s);
printf("s+1 = %p",s+1\n");
//s+1 和 s 相差4字节 1个int类型的大小
return 0;
}
linux@
数组名[下标] 访问元素的本质: 相对于数组首地址进行地址偏移取内容
s[i] 等价于 *(s+i)
#include<stdio.h>
int main(int argc, const char *argv[])
{
int s[5] = {10, 20, 30, 40, 50};
//s[i] 等价于 *(s+i)
printf("s[0] = %d\n",*(s+0)); //10
printf("s[1] = %d\n",*(s+1)); //20
printf("s[2] = %d\n",*(s+2)); //30
printf("s[3] = %d\n",*(s+3)); //40
printf("s[4] = %d\n",*(s+4)); //50
//定义一个指针指向一维数组
int *p = s; //最常用的写法
int *p2 = &s[0]; //这种写法也可以
//int *p3 = &s; //不建议的写法,编译会警告
//对数组名做 & 操作 相当于对操作空间是一个元素的指针s进行升维
//成操作空间是一行元素的指针,类型不匹配会报警告
// 指针指向数组后 就可以通过指针来操作数组的元素了 有下面等价关系
// s[i] <==> *(s+i) <==> *(p+i) <==> p[i]
// 遍历一维数组
int i = 0;
for(i = 0; i < 5; i++){
//下面4种方式都可以遍历一维数组
printf("%d ",s[i]);
printf("%d ",*(s+i));
printf("%d ",p[i]);
printf("%d ",*(p+i));
}
//p可以被重新赋值和自增自减
for(i = 0; i < 5; i++){
printf("%d",*p);
p++;
//printf("%d",*s); //错误写法
//s++;
}
return 0;
}
上方代码中 s 和 p 的区别:
s是数组名,是一个地址常量,不能被赋值,也不能自增自减
p是指针,是一个变量,可以被重新复制,也可自增自减
思考:在32位 小端存储的主机上 下面的代码会输出什么?
int s[5] = {1,2,3,4,5};
int *p = (int *)((int)s+1);
printf("%x\n", *p);
// 2000000
64位的主机 int(s+1)强转时只能存储4个字节,但是64位的主机的指针大小是8个字节,强转时可能会截断数据
6.指针实现字符串自定义函数
1.用指针实现 strlen 函数的功能
#include<stdio.h>
unsigned int user_strlen(char *p){
#if 0
int count = 0;
int i = 0;
while(p[i] != '\0'){
count++;
i++;
}
return count;
#endif
#if 0
int count = 0;
int i = 0;
while(*(p+i)){
count++;
i++;
}
return count;
#endif
#if 0
int count = 0;
while(*p){
count++;
p++;
}
return count;
#endif
#if 0
int count = 0;
while(*p++){
count++;
}
return count;
#endif
char *q = p;
while(*p++);
return p-1-q; // -1的原因是 上面的循环结束时 p 指向'\0' 后一位
}
int main(int argc, const char *argv[])
{
char s[20] = "hello world";
printf("%d\n", user_strlen(s));//11
return 0;
return 0;
}
linux
2.使用指针实现 strcpy 函数的功能
#include<stdio.h>
unsigned int user_strcpy(char *p, char *q){
#if 0
while(*q){ //while(*q != '\0'){
*p = *q;
p++;
q++;
}
*p = *q;
#endif
#if 1
while(*p++ = *q++);
#endif
}
int main(int argc, const char *argv[])
{
char s1[20] = "hello";
char s2[20] = "abc";
user_strcpy(s1, s2);
printf("%s\n", s1); // abc
}
3.使用指针实现 strcat 函数的功能
#include<stdio.h>
unsigned int user_strcat(char *p, char *q){
#if 0
//先找目标字符串的'\0'
while(*p){
p++;
}
//循环赋值
while(*q){
*p = *q;
q++;
p++;
}
*p = *q; // '\0'也追加过去
#else
while(*p++);
p--; //p++ = '\0'时,p指向\后一位的地址,需-1
while(*p++ = *q++);
#endif
}
int main(int argc, const char *argv[])
{
char s1[20] = "hello";
char s2[20] = "abc";
user_strcat(s1, s2);
printf("%s\n", s1); // abc
}
linu
7.指针和二维数组
二维数组的数组名就是数组的首地址
操作空间是一行元素的大小 也称为 行指针
#include<stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
//操作空间是一行元素的大小,也称 行指针
printf("s = %p\n", s);
printf("s+1 = %p\n", s+1); // 相差16字节 4*sizeof(int) 一行元素
return 0;
}
//对二维数组的数组名取一次 * 操作 表示对指针进行降维
把操作空间是一整行元素的行指针降维成 操作空间是一个元素的 列指针
#include<stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
printf("*s = %p\n", *s);
printf("*s+1 = %p\n", *s+1); // 相差4字节 一个元素的大小
return 0;
}
对列指针再取 * 操作 表示操作数组的元素
#include<stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
printf("**s = %d\n", **s); // 1
printf("*(*s+1) = %d\n", *(*s+1)); // 2
printf("**(s+1) = %d\n", **(s+1)); // 5
printf("*(*(s+1)+2) = %d\n", *(*(s+1)+2)); // 7
printf("*(s[2]+3) = %d\n", *(s[2]+3)); // 12
//s[2]等价于*(s+2) *(*(s+2)+3)
return 0;
}
等价关系 s[i][j] <==> *(s[i]+j) <==> *(*(s+i)+j)
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
// s[i][j] <==> *(s[i]+j) <==> *(*(s+i)+j)
// 遍历二维数组
int i = 0;
int j = 0;
for(i = 0; i < 3; i++){
for(j = 0;j < 4; j++){
//printf("%d ", s[i][j]);
//printf("%d ", *(s[i]+j));
printf("%d ", *(*(s+i)+j));
}
printf("\n");
}
return 0;
}
定义一个普通的指针指向二维数组
一级指针可以保存二维数组的首地址 但是不能按行操作 。因为对一级指针 取 ** 是错误的
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
int *p = s;
printf("p = %p s = %p\n", p, s); // 一样的
// 虽然一级指针可以保存二维数组的首地址 但是不能按行操作
// 因为 对一级指针 取 ** 是错误的
// printf("p[1][2] = %d\n", p[1][2]); // 错误 相当于 *(*(p+1)+2)
// 通过指针p也可以访问二维数组的所有元素 因为二维数组的元素在内存上也是连续的
// 但是只能一个元素一个元素的访问
for(int i = 0; i < 3*4; i++){
printf("%d ", p[i]);
if((i+1)%4 == 0){
printf("\n");
}
}
return 0;
}
8.数组指针
本质是一个指针,指向一个二维数组,也叫做行指针
一般用于函数传参 将二维数组作为函数的参数传递时
格式:数据类型 (*数组指针名)[列宽];
#include<stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
// 定义一个数组指针 p 指向二维数组 sa
int (*p)[4] = s;
printf("p = %p s = %p\n", p, s); //p 和 s 的结果是相同的
return 0;
}
针指向二维数组后 有如下的等价关系
s[i][j] <==> *(s[i]+j) <==> *(*(s+1)+j) <==> p[i][j] <==> *(p[i]+j) <==> *(*(p+1)+j)
#include<stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},\
{5,6,7,8},\
{9,10,11,12}};
// 定义一个数组指针 p 指向二维数组s
int (*p)[4] = s;
printf("p = %p s = %p\n", p, s); //p 和 s 的结果是相同的
//遍历二维数组
int i = 0;
int j = 0;
for(i = 0; i < 3; i++){
for(j = 0; j < 4; j++){
//printf("%d ", s[i][j]);
//printf("%d ", *(s[i]+j));
//printf("%d ", *(*(s+i)+j));
//printf("%d ", p[i][j]);
//printf("%d ", *(p[i]+j));
printf("%d ", *(*(p+i)+j));
}
}
return 0;
}
int s[3][6] = {{1,2,3,4,5,6},\
{7,8,9,10,11,12},\
{13,14,15,16,17,18}};
int (*p)[4] = s+1;
printf("%d\n", p[1][3]); // 14
9.指针和字符串
字符串可以保存到字符数组中。
字符串常量在内存上的存储位置是字符串常量区,字符串常量区位于RO段(read only),所以字符串常量不能被修改。
数组在内存的存储位置是栈区,栈区的内容可以修改。
#include<stdio.h>
int main(int argc, const char *argv[])
{
char s1[10] = "hello";
s1[0] = 'H'; //栈区的内容是可以修改
char s2[10] = "hello";
printf("s1 = %p s2 = %p\n", s1, s2); // s1和s2的首地址不一样
char *p1 = "world";
char *p2 = "world";
//*p1 = 'W'; //错误用法,字符串常量区的内容只能读,不能修改
//指针p1和p2 指向字符串常量区的"world",保存的地址是一样的
printf("p1 = %p p2 = %p\n", p1, p2); //相同地址
return 0;
}
10.指针数组
本质是一个数组,数组中每个元素都是一个指针
格式:
数据类型 *指针数组名[下标];
int *s[3]; //定义一个名为s的指针数组,数组中有5个类型为int *的指针变量
操作数组中的一个元素,和操作一个int *的指针用法相同
#include<stdio.h>
int main(int argc, const char *argv[])
{
char name_1[4][60] = {
"zhangsan",
"lisi",
"giannis antetokounmpo",
"zhaoliu"
};
printf("sizeof(name_1) = %ld\n", sizeof(name_1)); // 240
printf("%s\n", name_1[0]);
printf("%s\n", name_1[1]);
printf("%s\n", name_1[2]);
printf("%s\n", name_1[3]);
//可用二维数组处理多个字符串,但上面代码会造成空间的浪费,每个字符串都以最长的为主
//此时就可以使用指针数组来处理
char *name_2[4] = {
"zhangsan",
"lisi",
"giannis antetokounmpo",
"zhaoliu"
};
//name_2中保存4个字符串的首地址,可以通过指针来处理
printf("sizeof(name_2) = %ld\n", sizeof(name_2)); // 32
printf("%s\n", name_2[0]);
printf("%s\n", name_2[1]);
printf("%s\n", name_2[2]);
printf("%s\n", name_2[3]);
return 0;
}
11.二级指针
二级指针是用来保存一级指针的地址的
一般用于将一级指针的地址作为函数的参数传递时
int a = 10; //变量
int *p = &a; //一级指针
int **q = & p //二级指针
变量 一级指针 二级指针 的关系
等价关系:
**q <==> *p <==> a
*q <==> p <==> &a
q <==> &p
二级指针的用法
#include<stdio.h>
int main(int argc, const char *argv[])
{
int a = 10;
int *p = &a;
int **q = &p;
// 有了上述代码 就有了如下的等价关系
// a <==> *p <==> **q
// &a <==> p <==> *q
// &p <==> q
printf("a = %d *p = %d **q = %d\n", a, *p, **q); //10 10 10
printf("&a = %p p = %p *q = %p\n", &a, p, *q); //地址相同
printf("&p = %p q = %p\n", &p, q); //地址相同
// 通过二级指针q也可以访问到a
**q = 20;
printf("a = %d, **q = %d\n", a, **q); // 20
return 0;
}
不能使用一级指针来保存一级指针的地址 // 因为一级指针没法取 ** 操作
12.指针和数组表达式的练习
已知条件:
char *p = "hello world";
char a[] = "hello world";
char *s;
char b[20];判断: //如果正确 说明表达式的含义 如果错误 说明错误点
p++; // 正确 p是 char * 类型的指针,操作空间是1字节,p++表示向后偏移1字节,指向字母e
*p++; // 正确 *p++表达式结果是字母h,p指向字母e
(*p)++; // 错误,字符换常量不能被修改
*p = 'H'; // 错误,字符换常量只读,不能写操作
p = "abc"; //正确,指针指向"abc"的地址
a++; //错误,a是数组名,是地址常量,不能被赋值,也不能自增自减
*a++; //错误,a不能自增(*a)++; //正确,等价于a[0]++
*a = 'H'; //正确,*a等价于a[0],a存储在栈区,可以被赋值
a = "abc"; //错误,数组在定义后,不可再赋值
s++; //正确,s是野指针,s++也是野指针,对程序无影响
*s = 'H'; //错误,指s是野指针,指向空间不确定,操作野指针结果不可预知
s = p; //正确,指针变量相互复制,s指向"hello world"
b = a; //错误,数组名是常量,不可再赋值 。如果要赋值可使用strcpy(b,a);