C语言指针精简版(一)

简介: C语言指针精简版(一)

理解内存、地址与指针之间的挂关系

       我们假设这里有一栋宿舍楼,楼里有很多个房间,每个房间都有自己的门牌号,每个房间中又会有多个床位。你的朋友正在这栋宿舍楼中的某个房间的某个床位上等你,那么你必须要做的就是知道你朋友的门牌号这样就可以快速的找到你的朋友,把上面的例子对照到计算机中,又是怎样的呢?

       我们知道CPU在处理数据的时候,这些数据都是在内存中读取的,处理后的数据也会返回到内存中,而我们买电脑的时候,电脑上的内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?

        其实就是将内存划分为一个个的内存单元,每个内存单元的大小取一个字节,这里的每个内存单元都相当于一个房间,内存单元中的八个比特位就相当于房间里的八个床位,既然每个内存单元都可以看作是是一个房间,那么这个内存单元就应该有它的门牌号,有了这个门牌号CPU就可以快速找到它所对应的内存空间。在计算机中我们把内存单元的编号(门牌号)称为地址,C语言中给存储地址工具的起了个新名字:指针 

编址与寻址(简单理解)

编址:

       存储器是由一个个存储单元构成的,为了对存储器进行有效的管理,就需要对各个存储单元编上号,即给每个单元赋予一个地址码,这叫编址。经编址后,存储器在逻辑上便形成一个线性地址空间

寻址:

存取数据时,必须先给出地址码,再由硬件电路译码找到数据所在地址,这叫寻址

取地址操作符&

#include <stdio.h>
int main()
{
  int a = 10;
  return 0;
}

       在c语言中我们创建变量的过程其实就是在向内存申请一片内存空间,以int a = 10为例,我们可以看到a向内存申请了四个字节用于存放整数10:

0x000000ABECAFF7C4

0x000000ABECAFF7C5

0x000000ABECAFF7C6

0x000000ABECAFF7C7

通过取地址操作符得到整型变量a的地址:

#include <stdio.h>
int main()
{
  int a = 10;
  printf("%p\n", &a);
  return 0;
}

我们发现只取出了一个地址,这是因为取地址操作符获取地址时规定了只获取申请的最小地址

解引用操作符*

       上面我们通过&拿到a的地址在内存空间中申请的最低的地址:0x000000ABECAFF7C4 ,这个数值(地址)有时候也是需要存储起来⽅便后期再使⽤的,我们将它存储在指针变量中。

#include <stdio.h>
int main()
{
 int a = 10;
 int* pa = &a;//取出a的地址并存储到指针变量pa中
 return 0;
}

指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址

通过*我们就可以改变a在内存空间中存储的值:

#include <stdio.h>
int main()
{
 int a = 100;
 int* pa = &a;  //指针变量存储整型变量a的地址
 *pa = 20;      //通过对指针变量的解引用可以修改整型变量a内存空间中存储的值
 printf("%d",a);
 return 0;
}

指针变量的大小

       32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。

       同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的⼤⼩就是8个字节。

#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;
}

 

结论:

• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节

• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节

• 指针变量的⼤⼩和类型⽆关,只要是指针类型的变量,同平台下,⼤⼩相同

指针变量类型的意义

       指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?

//代码一
#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 = (char *)&n;
 *pc = 0;
 return 0;
}

调试后我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0

结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)

#include <stdio.h>
int main()
{
  int n = 10;
  char* pc = (char*)&n;
  int* pi = &n;
  printf("&n   = %p\n", &n);
  printf("pc   = %p\n", pc);
  printf("pc+1 = %p\n", pc + 1);
  printf("pi   = %p\n", pi);
  printf("pi+1 = %p\n", pi + 1);
  return 0;
}

     我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤

const修饰指针变量

const修饰变量

#include <stdio.h>
 int main()
 {
 int m = 0;
 m = 20;//m是可以修改的
 const int n = 0;
 n = 20;//n是不能被修改的
 return 0;
}

       上述代码中n是不能被修改的,因为n被const修饰后,在语法上加了限制,只要我们在代码中对n进行修改,就不符合语法规则,就会报错。

       那如果我们绕过n,使⽤n的地址,去修改n可以吗?(虽然这样其实并不符合语法规则)

#include <stdio.h>
int main()
{
 const int n = 0;
 printf("n = %d\n", n);
 int*p = &n;
 *p = 20;
 printf("n = %d\n", n);
 return 0;
}

       我们可以看到这⾥n被修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?

const修饰指针变量

#include <stdio.h>
测试一
//void test1()
//{
//  int n = 10;
//  int m = 20;
//  int* p = &n;
//  *p = 20;    //ok
//  p = &m;     //ok
//  printf("%d\n", m);
//  printf("%d\n", n);
//}
测试二
//void test2()
//{
//  int n = 10;
//  int m = 20;
//  const int* p = &n;
//  *p = 20;    //no
//  p = &m;     //ok
//  printf("%d\n", m);
//    printf("%d\n", n);
//}
测试三
//void test3()
//{
//  int n = 10;
//  int m = 20;
//  int* const p = &n;
//  *p = 20;    //ok
//  p = &m;     //no
//}
测试四
//void test4()
//{
//  int n = 10;
//  int m = 20;
//  int const* const p = &n;
//  *p = 20;    //no
//  p = &m;     //no
//}
int main()
{
  //测试⽆const修饰的情况
  /*test1();*/
  测试const放在*的左边情况
  /*test2();*/
  测试const放在*的右边情况
  /*test3();*/
  测试*的左右两边都有const
  /*test4();*/
  return 0;
}

结论:

  • const在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本⾝的内容可变
  • const在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变

!!!只要位于*左侧或者右侧即可,并不要求具体位置!!!

指针运算

指针-整数

数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素

#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;
}
//字符类型指针也一样
#include <stdio.h>
int main()
{
    char arr[] = "abcdef";
    char* pc = &arr[0];
    while (*pc != '\0')
    {
        printf("%c ", *pc);
        pc++;
    }
    return 0;
}

注意在内存监视窗口中选择不同列时左侧的地址情况是不同的:

指针-指针

//指针-指针
#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;  //这里的数组名就相当于数组首元素地址
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p); //p指向某个地址,通过*p拿到该地址中存储的值
 p++;    //p++等于地址++
 }
 return 0;
}

野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

造成野指针的情况有三种:指针未初始化、指针的越界访问、指针指向的空间释放

指针变量未初始化

#include <stdio.h>
int main()
{
int* p;//整型的指针变量未初始化,默认为随机值
*p = 20;
return 0;
}      //结果报错

指针的越界访问

#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;
}

指针访问越界就会导致栈溢出问题

指针指向的空间释放(下面程序运行后仍能输出200)

#include <stdio.h>
int* test()   //因为返回的是一个地址,所以返回类型应该是int*类型
{
  int n = 100;
  return &n;
}
int main()
{
  int* p = test();   //用指针变量p接收返回回来的地址
  *p = 200;
  //n出函数释放内存空间,但是p指针仍然保存了n内存空间的地址
    //这时如果再使用*p=200就会出问题,此时p就为野指针
  printf("%d\n", *p);
  return 0;
}

       通俗来讲就是:相当于你今天开了个住一晚的酒店房间,但是你第二天走后告诉另一个人这个房间还可以住,你让你朋友去住那个房间,虽然这个房间你还能进去但是里面的东西已经被保洁阿姨打扫过了没有你朋友住过的痕迹了。

如何规避野指针

主要是一些具体的操作方式,涉及因为检查不仔细导致的问题不予描述

指针的初始化

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;//当我们还没有规定该指针指向哪里的时候,即使将该指针赋值为NULL
 return 0;
}

assert宏(断言)

作用:确保程序符合指定条件,如果不符合,就报错终⽌运行程序

包含头文件:assert.h

使用方式:assert(表达式);

表达式为真, assert() 不会产⽣任何作⽤程序继续运⾏

表达式为假, assert() 就会报错

好处:

1、⾃动标识⽂件和问题所在⾏号

       当表达式为假时,assert()会在标准错误流 stderr 中自动写⼊⼀条错误信息:显⽰没有通过的表达式,以及该表达式所在文件的⽂件名和⾏号

#include <stdio.h>
#include <assert.h>
int main()
{
  int* p = NULL;
  assert(p != NULL);
  return 0;
}

拥有⽆需更改代码就能开启或关闭 assert宏的机制

      如果已经确认程序没有问题,不需要再做断⾔,就在 #include  语句的前⾯,定义⼀个宏 NDEBUG :

#define NDEBUG
#include <assert.h>

       然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,就重新启⽤了 assert() 语句。

缺点:因为引⼊了额外的检查,增加了程序的运⾏时间

       ⼀般我们在debug版本中使⽤,这样有利于程序员排查问题,如果在rekease版本使用会影响⽤⼾使⽤时程序的效率

指针的使⽤和传址调⽤

传址调用

如果要写一个交换两个整型变量的值的函数,我们可能会这样写:

#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_s("%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;
}

但是它们并未产生实际的调用效果,调试一下看看:

       我们发现a的地址是0x000000b39e3bf884,b的地址是0x000000b39e3bf8a4,在调⽤Swap1函数时,将a和b的值传递给了Swap1函数,在Swap1函数内部创建了形参x和y来接收a和b的值,但是x的地址是0x000000b39e3bf860,y的地址是0x000000b39e3bf868,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量的值传递给了函数,这种调⽤函数的方式叫做:传值调⽤

结论:形参是实参的一份临时拷贝,对形参的修改不影响实参

传值调用

那怎么办呢?

       我们现在要解决的就是当调⽤Swap1函数的时候,Swap1函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就需要使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap1函数⾥边通过地址间接的操作main函数中的a和b就好了:

#include <stdio.h>
void Swap1(int* px, int* py)
{
  int tmp = 0;
  tmp = *px;
  *px = *py;
  *py = tmp;
}
int main()
{
  int a = 0;
  int b = 0;
  scanf_s("%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;
}

       我们可以发现,此时px、py与a、b的地址就相同了,这样就可以实现在Swap1函数中直接修改a和b的值,这种函数调⽤⽅式叫:传址调⽤

~over~

相关文章
|
1月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
96 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
1月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
61 9
|
1月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
48 7
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
186 13
|
2月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
2月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
152 3
|
2月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
47 1
|
2月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
2月前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。