C语言 指针的基础知识

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分给此操作的                             内存
#endif

    return 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);

// 200000064位的主机 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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值