C语言-指针1

指针是什么
指针定义和使用
指针变量的运算
指向数组的指针
字符串指针

C语言指针到底是什么?

计算机中所有数据都需要放在内存中,不同类型的数据在内存中存放的字节数也各不相同,而我们为了表达清楚各个数据在内存中的位置,为数据在内存中的字节编上了号码。每个字节的号码都是不一样的,根据号码就可以找到你想要的字节。而我们将这种号码称之为地址(Address)或者指针(Pointer)。就像我们用输入函数时的&符号,它是取出我们输入的值放入我们的命名的变量的地址中。

一切都是地址

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能被CPU调用。

CPU访问内存时需要的是地址,不是变量名或是函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序时,他们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

变量名和函数名是为了给我们提供方便,不用去直面二进制地址。需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写程序时,我们认为变量名表示的是数据本身,而函数名,字符串名和数组名表示的是代码块或数据块的首地址。

指针

数据在内存中的地址也称为指针,如果一个变量存储了一个数据的指针,那么这个变量就称为指针变量。

在C语言中,允许用一个变量来存放指针,也就是指针变量,而指针变量中的值就是数据的地址,这样的一份数据可以是数组、字符串、函数、也可以是另外的一个普通变量或指针变量。

定义指针变量

定义指针变量与定义普通变量非常的类似,不过要在变量名前加星号*,格式为:

datatype *name;

datatype 表示这个指针变量的数据类型。

它和普通的变量并没有特别大的区别,只是它其中存的不是数值,而是地址,我用一段简单的代码让大家了解一下指针变量和普通变量的区别

//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;

号是一个特殊符号,表明一个变量是指针变量,定义p1、p2时必须带上,而给p1、p2赋值时,因为已经知道它是一个指针变量,就没有多此一举再带上*号了,后边可以像使用普通变量一样来使用指针变量。

而上面的程序变化情况如下图:

需要强调的是p1、p2的类型分别是float*char*,而不是float和char,他们是完全不同的数据类型。

指针变量也可以连续定义:

int *a, *b, *c;
//或者只定义一个
int *a, b, c;

通过指针变量取得数据

这里的*称为指针运算符,用来取得某个地址上的数据。

#include <stdio.h>
int main(){
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值
    return 0;
}

运行结果:15,15

我们说过类似于这样赋值,是将a的地址赋值给了p这个指针变量,而*p则表示取得这个地址上的数据,也就是变成了取a上的数据,也就是说,a这个变量名是直接通过地址取得自己的数据。而*p,p存的是a这个变量的内存地址,是*p是获取p存的地址上的值。

也就是说*p是间接获取数据,而a是直接后取数据,前者比后者的代价要高。

指针除了能读这个地址上的数据还拥有修改的权力。

*号在不同的场景下有不同的作用:*号可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加上一个*表示获取指针指向的数据,或者说指向这个数据的本身。

明白指针变量最主要的是要明白这个指针到底指的是数据还是地址

也就是说,定义指针变量时的*和使用指针变量时的*意义完全不一样,例如:

int *p = &a;
*p=100;
//他还有另一种赋值的写法
int *p;
p = &a;
*p = 100;

两段程序表示的是一个意思。但是给指针变量本身赋值时不能加上*

指针变量也可以出现在普通变量能出现的任何表达式中,比如:

int x, y, *px = &x, *py = &y;
y = *px + 5;//表示把x的值加5赋值给y
y = ++*px;//表示将x中的值加一之后赋给y,++*px相当于++(*px)
y = *px++;//相当于y = *(px++)
py = px;//是将一个指针中存的值赋给另一个指针变量

我们来重申一下*&的关系

int a, *pa = &a;

那么*&a&*pa分别是什么意思嘞?

  1. *&a可以理解为*(&a),而其中&a就是表示变量a的地址,那也就是说等价于pa,那如果是这样我们把pa带入到原来的表达式中,那么原来的表达式就可以表示为*pa,也就是说绕来绕去取得还是a中的值。

  2. &*pa可以理解为&(*pa),而其中的*pa指向的时a中的数据,那么我们就可以把a代入回原来的式子中,也就时变成了&a也就是表示取a中的地址。也就是说&*pa等价于pa

在这里我不得不对*做一个小总结

在我们目前学习的语法中星号*主要有三种用途:

  • 表示乘法,这个我就不做过多阐述了
  • 表示一个指针变量,以便普通变量分开
  • 表示获取指针指向的数据,类似于一种间接性的操作

指针变量的运算

指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算。 例如:

#include <stdio.h>
int main(){
    int    a = 10,   *pa = &a, *paa = &a;
    double b = 99.9, *pb = &b;
    char   c = '@',  *pc = &c;
    //最初的值
    printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //加法运算
    pa++; pb++; pc++;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //减法运算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
    //比较运算
    if(pa == paa){
        printf("%d\n", *paa);
    }else{
        printf("%d\n", *pa);
    }
    return 0;
}

运行结果:

&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784

从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。

我们可能会想到,我们的指针变量为什么不是简单的加一减一,反而会跟数据类型扯上关系呢,我们都知道在数据类型中整形占4个字节,双精度浮点型占8个字节,字符型占1个字节,当我们的指针在移动是,如图所示 以整形指针和整形数据为例 如果地址只移动1,会产生什么效果 而当它移动的是一个整形数据类型的占位时

不过对于C语言来说,单独定义的变量并不能保证它们之间存放的内存地址是相互挨着的,他们也有可能是分散的,如果只是指向普通变量的指针,我们并不会对其进行加减法运算,虽然不会报错,但是没有意义,因为我们不知道你命名的变量后的是什么数据。

一定要记住,指针变量是不能进行除加减法以外的所有运算,除了会发生错误没有其他任何意义。

数组指针

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第0个元素。在C语言中,我们将第0个元素的地址称为数组的首地址

数组名本意是表示整个数组,也就是表示多份数据的集合,但在使用过程中经常会转换为指向数组第0个元素的指针,所以上面使用了“认为”一词,表示数组名和数组首地址并不总是等价。初学者可以暂时忽略这个细节,将数组名就当作第0个元素的指针去使用。

我们用一个简单的程序来解释一下数组指针的应用。

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int *parr = arr;
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    for(i=0; i<len; i++){
        printf("%d  ", *(parr+i) );  //*(parr+i)等价于arr[i]
    }
    printf("\n");
    return 0;
}

运行结果:

99  15  100 888 252
99  15  100 888 252
  • sizeof(arr)能计算出整个数组所占用的字节数。求出数组长度控制循环次数。
  • 在这里arr就代表字符串的首地址,所以不需要加&,也可以取到数组的首地址。
  • *(arr+i)这个表达式,arr也是另一种类型的指针,能够指向数组首元素的指针,所以它也等价于arr[i]。
  • 上面的例子中*parr指向的是int类型,所以指针应该是int*类型。 我们在上述程序中的指针*parr指向了数组arr,我们引用数组分为几种方式:
    • *(parr+i) == a[i] == *(a+i) == parr[1],这几种引用数组中的值都是OK的,都是指向数组中第二个值。
  • *p++ 等价于*(p++)*的优先级和自增自减的优先级是一样的,所以当他们出现在一起时,从右往左结合,先算右边,再往左结合。
  • ++*p就是先取指向数组的值,然后将其中的值自增。

C语言字符串指针

除了我们经常定义的字符数组外,还用一种定义字符串的方式,就是直接使用一个指针直接指向字符串,例:

char *str = "https://tianlangz.top";
// 还有另一种写法
char *str;
str = "https://tianlangz.top";

字符串中所有字符在内存中都是连续排列的,str指向的是字符串的第0个字符;我们通常将第0个字符的地址称为首地址。字符串中每个字符类型都是char,所以str的类型与必须是char*,例如:

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "https://tianlangz.top";
    char *pstr = str;
    int len = strlen(str), i;
    //使用*(pstr+i)
    for(i=0; i<len; i++){
        printf("%c", *(pstr+i));
    }
    printf("\n");
    //使用pstr[i]
    for(i=0; i<len; i++){
        printf("%c", pstr[i]);
    }
    printf("\n");
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    return 0;
}

运行结果:

https://tianlangz.top
https://tianlangz.top
https://tianlangz.top

这样看起来它和普通数组没有任何区别,能调用,能读取,能使用,任何方式都一样,但是,它和字符数组有一个最根本的区别就是它们在内存中的存储区域是不一样的,字符数组的存储区域在全局数据区或栈区,第二种形式的字符串存储在常量区,简单地说字符数组(全局数据区或栈区)是拥有读取和写入权限的,而我们所定义的字符串指针存在于常量区的字符串,他虽然拥有读取的权限,但并不具备修改的权限,没有写入权限。

内存权限的不同导致了字符数组可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改了,任何对它的赋值操作都是错误的。

举个例子:

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "I love C!";  //正确
    str[3] = 'P';  //错误
    return 0;
}

这段代码虽然能执行和链接,但是运行时会出现段错误(Segment Fault) 或者写入位置错误。

第四行代码是正确的,它改变了指针变量本身的指向,所以能做到更改,但是不能修改字符串中的字符。

那我们该什么时候使用字符数组,什么时候使用字符串常量呢?

在编程中如果我们只需要涉及到字符串的读取,那么我们可以用字符串常量,如果涉及到修改,那么只能用字符数组。

Posts in this Series