C语言(六)----指针(上)

深入理解指针(1)

内存和地址

内存单元都有自己的编号,编号也叫地址,房间号就是地址,地址在C语言中又叫指针

一个内存单元:一个字节(8个bite)

取地址操作符&

变量创建的本质是在内存中申请空间

&(取地址操作符(属于单目操作符))在有多个字节时,取的是其中较小的地址

打印地址:%p

#include
int main()
{
	int a = 0;
	printf("%p ",&a);
	return 0;
}
//&a取出的是a所占4个字节中地址较⼩的字节的地址。虽然整型变量占⽤4个字节,但是我们只要知道了第⼀个字节地址,就能访问到4个字节的数据。

指针变量的概念

用来存放指针(地址)的变量

解引用操作符(*)

通过*就可以通过地址找到地址所指向的对象

#include
int main()
{
	int a = 10;
	int* pa = &a;
    //打印结果一样
	printf("%p\n",&a);
	printf("%p\n",pa);
    //打印结果一样
	printf("%d\n",a);
	printf("%d\n",*pa);    
	return 0;
}

//也可以通过指针修改a的值
int a = 10int* pa = &a;//此处的*说明pa是指针变量
*pa = 20;//此处的*是解引用操作符(属于单目操作符),此处的*pa == a,*pa = 20就将a的值改为20了

指针变量的大小

地址的存放需要多大的空间,那么指针变量的大小就是多少

指针变量的大小与类型是没有关系的

32位平台下地址都是4个字节,即指针变量的大小是4个字节(x86环境)

64位平台下地址都是8个字节,即指针变量的大小是8个字节(x64环境)

指针变量类型的意义

指针的解引用过程

指针的类型决定了,对指针进行解引用的时候有多大的权限(即一次能操作几个字节)

指针±整数

指针类型决定了指针向前或向后走几个字节

#include  
int main() 
{
	int n = 10;
	char *pc = (char*)&n;
	int *pi = &n;
	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	printf("%p\n", pi);
	printf("%p\n", pi+1);
    return 0; 
}
//运行结果:
000000FE1A6FFA14
000000FE1A6FFA14
000000FE1A6FFA15
000000FE1A6FFA14
000000FE1A6FFA18
void*指针

没有具体的类型(或者叫泛指针),

优点是:可以接受任意类型的地址,

局限性是:不能进行指针±整数和指针的解引用过程

指针运算

指针±整数
#include  
int main() 
{
    int arr[] = {1,2,3,4,5};
    int* pa = arr;
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for(i = 0;i < sz;i++)
    {
        printf("%d ", *(pa + i));//此处的p+i就是指针+-整数
    }
    printf("\n");
    return 0; 
}
指针-指针

就是地址减去地址

指针减去指针时,两个指针一定是指向同一块区域的,所得到的绝对值,是两个指针之间的元素个数

#include  
int my_strlen(char* s)
{
    char* p = s;
    while (*p != '\0')
    {
        p++;
    }
    return p - s;
}
int main()
{
    int r = 0;
    r = my_strlen("abc");
    printf("%d",r);
    return 0;
}
//结果为:3
指针的关系运算

指针的大小比较就是指针的关系运算(其实比较的就是地址的大小)

#include  
void Print_arr(int* arr, int sz)
{
    int* p = arr;
    while (p < arr + sz)
    {
        printf("%d ",*p);
        p++;
    }
}
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    Print_arr(arr, sz);
    return 0;
}

深入理解指针(2)

const修饰指针

const修饰变量

const是常属性----不能改变的属性

const int a = 10;//a变成了常变量,本质还是变量,但被const修饰,语法限制a不能再改变

//以下方法可以改变,原理是绕过a,使用a的地址修改a
//但是使用了const修饰a,就是不想a再被改变,不应修改
int* p = &a;
*p = 1;

//而如果对指针变量修饰,就无法再修改了
const int* p = &a;

p与*p

int a = 100;
int* p = &a;
*p = 0;
//p是一个指针变量,里面存的是地址
//*p是p指向的对象(a)
const修饰指针变量

const可以放在*的左边,也可以放在右边,但是意义不一样,可以同时存在

const放在*的左边,表示指针指向的内容不能通过指针来改变了,但是指针变量本身是可以改变的

const放在*的右边,表示指针变量本身不能被修改了,但是指针指向的内容是可以通过指针变量来改变

int a = 100;
const int* p = &a;
*p = 10;//不能发生
p = &a;//可以发生

int a = 100;
int* const p = &a;
*p = 10;//可以发生
p = &a;//不能发生

野指针

指针指向的位置不可知(随机的,不正确的,没有明确限制的)

野指针成因:
未初始化指针
#include  
int main() 
{ 
    int *p;//局部变量指针未初始化,默认为随机值 
    //正确初始化:int* p = NULL;
    *p = 20; 
    return 0; 
}
指针越界访问
#include
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int* p = arr;

	int i = 0;
	for (i = 0; i < 11; i++)
	{
		printf("%d ",*(p+i));
        //当i=10时循环继续,p+i就超过数组的最大范围,造成了越界,
        //一旦进行越界访问,p就成为了野指针
        //为了避免中指针越界,在对指针使用之前,可以进行判断
        //p = NULL;
        //if(p != NULL)
        //{
            //语句;
        //}
	}

	return 0;
}
指针指向的空间释放
#include  
int* test() 
{ 
	int n = 100; //n是创建的临时变量,n所占的4个字节,当函数test返回后,n的空间 就返还给了操作系统
	return &n;   //此处返回的是临时变量的地址
}
int main() 
{ 
	int* p = test();
	printf("%d\n", *p);
    return 0; 
}
避免野指针
  1. 初始化:如果不知道指针指向哪里,可以初始化为NULL
  2. 小心指针越界
  3. 指针不再使用时,及时置为NULL,指针使用之前,检查其有效性
  4. 避免返回局部变量的地址

assert断言

aeeert.h头文件定义了宏assert(),用于再运行时确保程序符合指定条件,如果不符合,就报错停止运行。这个宏就被称为断言

断言可以关闭。

#define NDEBUG//用于关闭断言
#include

assert(p != NULL);//如果p不等于NULL,则继续执行,
                  //如果等于,终止运行,并且给出报错信息的提示

指针的使用和传址调用

strlen的模拟实现

strlen用于求字符串长度,统计的是字符串中\0之前的字符个数

#include
#include
int My_Strlen(char* str)
{
	int count = 0;
    assert(str);//断言str不为空
	while (*str != '\0')//判断是否到\0,循环内计算\0之前的字符个数
	{
		count++;
		str ++;
	}
	return count;
}
int main()
{
	char* str = "abcdef";
	int len = My_Strlen(str);//传的是首地址
	printf("%d ",len);
	return 0;
}

传值调用和传址调用

传值调用
#include
void Swap(int x, int y)
{
	int temp = 0;
	temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	printf("交换前;a=%d b=%d\n", a, b);
	Swap(a, b);
	printf("交换后;a=%d b=%d\n", a, b);
	return 0;
}
//结果为:
//a=1 b=2
//a=1 b=2

将上述代码运行后我们可以发现,我们写的交换函数其实并没有将a和b的值交换,这是因为实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参对形参的修改不影响实参

传址调用

刚才我们在进行交换a和b的操作中,并没有交换a和b,因为我们在传参的时候,传的是a和b的值,即传值调用,那么怎样才能交换a和b呢?

其实只要我们在传参的时候将a和b的值传过去就可以了,这种方式就叫做传址调用

#include
void Swap2(int* x, int* y)
{
	int temp = 0;
	temp = *x;
	*x = *y;
	*y = temp;
}
int main()
{
	int a = 1;
	int b = 2;
	printf("交换前;a=%d b=%d\n", a, b);
	Swap2(&a, &b);
	printf("交换后;a=%d b=%d\n", a, b);
	return 0;
}
//结果为:
//a=1 b=2
//a=2 b=1

深入理解指针(3)

数组名的理解

数组名就是数值首元素的地址

//下面两行代码的结果是相同的
int* p = &arr[0];
int* p = arr;

数组名就是数值首元素的地址,但是有例外:

sizeof(数组名)//单独放到sizeof的括号中,数组名表示整个数组,计算出的是整个数组的大小

&数组名//这里的数组名也表示整个数组,取出的是这个数组的地址

&arr[0]   //首元素的地址    
arr       //首元素的地址
&arr      //数组的地址
    
&arr[0]+1   //首元素的地址    
arr+1       //首元素的地址
&arr+1      //数组的地址

使用指针访问数组

#include
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
    int* p = arr;
    int i = 0;
    for(i = 0;i<sz;i++)
    {
	   scanf("%d",p+i);
    }
    for (i = 0; i < sz; i++)
    {
	   printf("%d ", *(p + i));
     //printf("%d ", p[i];
    }
    return 0;
}
// int* p = arr;此处arr是数组首元素地址,赋值给了p,那么此处的arr与    p是等价的
//本质上p[i]等价于*(p+i)
//同理arr[i]等价于*(arr+i)

一维数组传参的本质

一维数组传参,函数的形参可以写成数组形式,也可以写成指针形式

void Fun(int* arr)
//此处的参数写成了数组形式,但是本质还是指针
void Fun(int arr[])
//此处参数写成指针行式    
//两者等价

数组在传参的时候,不会将整个数组传递过去,传递的是数组首元素的地址

所以在函数内没办法计算数组的元素个数,形参和实参的数组是同一个

冒泡排序

核心思想:两两相邻的元素进行比较

#include
void Bubble_Sort(int* arr, int sz)
{
	int i = 0;
	//n个数字需要比较n-1趟
    //每趟将会排序好一个数字,比较完成后,将较大的值置于数组末尾  
	for (i = 0; i < sz-1; i++)
	{
        int flag = 1;//假设已经有序了
		int j = 0;
		//sz-1之后再减i是因为每趟都会排序好一个数字,经过i趟后,
		//所需排序的数字就减少了i个
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
                flag = 0//发生交换,证明无序
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
            if(flag == 1)//第一趟没交换证明有序,后续就不用排序了
               break;
		}
	}
	printf("\n");
}

void Print_Arr(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main()
{
	int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//排序前打印一次
    Print_Arr(arr, sz);
	Bubble_Sort(arr,sz);
	//排序后打印一次
    Print_Arr(arr, sz);

	return 0;
}

二级指针

指针变量也是变量,是变量就有地址,指针变量的地址就存放在二级指针中,

即二级指针变量是存放一级指针变量的地址的

int a = 10;
int* p = &a;  //一级指针变量
int** pp= &p; //pp就叫二级指针,int*是说pp指向的p是int*类型的,
              //*是说pp是指针变量,*pp=p,*p=a-->*(*pp)=a
//类似一级指针的
const int **pp
int * const *pp
int ** const pp

指针数组

类比

字符数组:char arr[5]----存放字符的数组

整型数组:int arr[5]----存放整型的数组

那么指针数组就是存放指针(地址)的数组

#include
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;

	int* arr[3] = { &a,&b,&c };//存放整型指针的数组
	int** p = arr;//将数组arr的地址传给p
	
	int i = 0;

	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(arr[i]));
	}
	printf("\n");

	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(*(p+i)));
        //对(p+i)解引用得到*(p+i),
        //*(p+i是a,b,c的地址,再次解引用得到*(*(p+i)),
        //*(*(p+i))就是a,b,c内存中存储的对应的值10 20 30
	}
}
//结果为:
//10 20 30
//10 20 30

指针数组模拟二维数组

#include
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 6,7,8,9,10 };
	int arr3[] = { 11,12,13,14,15 };

	int* arr[3] = { arr1,arr2,arr3 };

	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

你可能感兴趣的:(c语言)