C/C++ 学习笔记四(指针、数组)

指针

什么是指针?

指针其实是内存单元地址

什么是指针变量?

指针变量是用于存储内存单元地址的变量

指针变量存储的实质

顾名思义,指针变量存储的是内存单元的值,即存储的值其实指向的一个特定类型内存区域的起始地址。

如下例子,整型变量a的值为123,其内存单元地址为0x104,指针变量存储的值是变量a的内存单元0x104。

指针变量指向了内存地址起始为0x104,长度为4个字节的内存单元。p也可以对这个4个单元进行操作。

int a = 123;
int *p = &a;

指针变量的运算

c语言中,我们可以像普通整型数据一样,对指针变量进行运算。

指针变量的转换

普通类型的指针变量都可以直接赋值void *空类型指针, 但空类型指针需要强转才可以转成普通类型指针。

void 指针是一种特殊的指针,表示“无类型的指针”,因为其没有指定类型,所有它可以指向任何类型的数据,也就是说任何类型的指针可以直接复制给void指针。

void * vp;
int * ip;
...
vp = ip;

但是void指针赋值给其他类型的指针时,必须强制转换

void *vp;
int * ip;
...
ip = (void *)vp;

这是因为指定了类型的指针变量指向了内存的一块区域,但空类型指针无法确定指向内存区域的大小。

需要注意的是,不同类型指针变量相互转换时,需避免访问非法的内存区域,导致程序异常退出。 如下例子,chP指向了一段长度为1字节的变量a的内存区域,当其强制转换成int指针时,则超出了编译器分配的内存区域,程序会异常退出。

    char  a = 'a';
    char * chP = &a;
    int * iP = (int *)chP;
    *iP = 123;

指针变量的算术运算

指针变量可以使用算术运算符实现自增减,如下例

    int a = 123;
    int * ip = &a;
    printf("p = %p \n", ip);

    char * charPtr = (char *)ip;
    double * doublePtr = (double *)ip;
    charPtr++;
    doublePtr++;
    printf("charPtr = %p \n", charPtr);
    printf("doublePtr = %p \n", doublePtr);

假设&a地址为0x100,则上面例子的输出为

p = 0x100
charPtr = 0x101
doublePtr = 0x108

即使指针变量的算术运算为增减sizeof(数据类型)的大小。上例子中p的值为0x100,强转为char 后自增1,结果为0x101,强转为double 后自增1为,0x108(0x100+0x8)。

同理因为空指针类型无法得知其指向区域的长度,void *指针便无法进行增减操作。

数组

C语言中,数组与指针是一种非常暧昧的关系,因数组和指针经常可以相互的转换,所以经常会将其两者混淆。 真正的事实是,两者拥有不同的存储结构,但引指针的灵活性,两者可以相互的引用、转换。

数组的存储结构

与指针的存储结构相比,数组在内存中占据的是连续的字节单元。即指针存储的长度根据计算机不同,是一个固定的大小 (32位4个字节、64位8个字节),数组存储的是一块连续的内存区域。

    int * p =  ....;
    int a[3] = { 1,2,3 };
    printf("sizeof(p) :%d \n",sizeof(p));   //sizeof(p) :4
    printf("sizeof(a) :%d \n", sizeof(a));  //sizeof(p) :12

为什么指针可以访问数组的元素?

那为什么指针可以访问数组中元素?

这是因为数组标识符表示的是该数组的第一个元素的地址,当指针指向数组标识符时,便可以通过指针访问数组中的各个元素。

    int a[3] = {1,2,3};
    int * p = a;

    printf("a :%p \n",a);
    printf("* p  :%p \n", p);
    printf("* a[1]  :%p \n", *(p++)); //1

函数调用时数组作为参数时为地址传递

C语言的标准中规定:所有的数组在作为参数传递时,都转换成指向数组起始地址的指针,其他参数均采用值传递。

采用地址传递好处是形参不存在存储空间,编译系统不为形参分配内存,数组名或者指针便是一组连续空间的首地址。

例如下面例子输出的size都为4。

void fp(char *arr)
{
    printf("fp size : %d", sizeof(arr));
}

void fa(char arr[])
{
    printf("fa size : %d", sizeof(arr));
}
int main()
{
    char c[] = "Hello";
    fp(c);  // fp size :4
    fa(c);  // fa size :4
}

数组与指针的区别

数组和指针其实并不是一个相同概念,虽然在日常的使用中,经常可以使用指针代替数组,用于遍历数组的元素,例如

char array[6]="hello";
char *chPtr = array;
char charE = chPtr[1];
printf("%c \n",charE);

再来一个例子,可以证明数组并不是指针:

/* 文件 f1.c*/
int a[3] = {1,2,3};
/* 文件 main.c */
#include "f1.c"
int main()
{
    extern int * a;
    return 0;
}

执行上面代码会提示“a”: 重定义;不同的间接寻址类型 在使用extern int a时,编译器认为a是一个在外部声明的整型指针变量,但f1.c中,a是一个长度为3的整型数组,在32位系统系统下,int 长度为4字节,而int [3]长度为4*3 = 12字节。

再看一个例子

    char * a = "hello";
    char arr[6] = "hello";
    if (a == arr)
    {
        printf("same");
    }
    else {
        printf("different");
    }

程序执行后打印different。这是因为a指向的是存储于数据段静态变量hello,arr则指向编译器分配的内存空间。两者的值并不一样。

对于数组而言,编译器已经为数组分配了一定的空间以及对应的地址,通过数组地址的偏移,可以访问该数组的元素。 而指针,编译器为其分配了空间,用于存储地址值。而对于存放在其中的值,只有在程序运行时才能知道。

使用指针和数组的注意点与建议

1. 使用指针前必须初始化,否则会指向错误的内存区域,导致程序异常。

2.使用NULL指针作为函数调用失败的返回值。类似malloc函数,当分配内存失败时返回NULL,用以表示为一种异常情况。

3.在不知内存区域具体类型情况下,避免对void 指针进行算术操作

例如,下例子对p任何的操作都会导致程序出错

void *p;
p++;
p--;

4. 不同类型指针转换时,注意不超出编译器分配的内存区域。

如下例子,ip使用了超出了编译器分配的内存,会导致程序异常退出

char a = '2';
char * cp = &a;
int *ip = (int *)cp;
*ip = 123;

5.避免指针和整型数据的直接转换。

总结

1.指针变量是变量,存储内存地址的变量。 3.数组存储的是一段连续的内存区域 4.数组标识符存储了,一段内存区域的起始地址 5.数组作为参数传递时是地址传递,其他类型则为值传递

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我和PYTHON有个约会

13.1 函数中的变量

在函数中,我们可以看到也进行了变量的使用,那函数中的变量和函数外的变量到底有什么区别呢?

652
来自专栏LanceToBigData

JavaSE(六)包装类、基本类型和字符串之间的转换、==和equals的区别

一、包装类 Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足, 在设计类时为每个基...

1657
来自专栏LanceToBigData

JavaSE(八)集合之Set

今天这一篇把之前没有搞懂的TreeSet中的比较搞得非常的清楚,也懂得了它的底层实现。希望博友提意见! 一、Set接口 1.1、Set集合概述   Set集合:...

2075
来自专栏大闲人柴毛毛

稳扎稳打JS——this

this的值是在运行时确定的 JS中的this究竟代表什么,这是在程序运行时根据上下文环境确定,可以分为以下几种情况。 1. 全局作用域中的this 在全局作...

3545
来自专栏逻辑熊猫带你玩Python

Python | 6大数据类型方法归纳总结(中)

可以直接使用tuple()创建一个新的元组,或者,使用tuple()将一个对象转换成元组。

984
来自专栏Golang语言社区

[基础篇]Go语言变量

变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。变量可以通过变量名访问。 Go 语言变量名由字母、数字、下划线组成,其中首个字母不能为数字。 声...

3537
来自专栏python学习之旅

Python笔记(九):字符串操作

(一)    字符串 单引号、双引号、三重引号都可以作为字符串的开始和结束,三重引号可以直接输入多行字符串。三重引号可能一般是用来写多行注释。 ? (二)   ...

3657
来自专栏零基础使用Django2.0.1打造在线教育网站

关于JAVA你必须知道的那些事(三):继承和访问修饰符

今天乘着还有一些时间,把上次拖欠的面向对象编程三大特性中遗留的继承和多态给简单说明一下。这一部分还是非常重要的,需要仔细思考。

753
来自专栏Java帮帮-微信公众号-技术文章全总结

【选择题】Java基础测试二(15道)

【选择题】Java基础测试二(15道) 11.对于构造方法,下列叙述正确的是:(AC) A. 构造方法的方法名必须与类名相同; B. 构造方法必须用void...

36310
来自专栏水击三千

浅谈JavaScript的面向对象程序设计(一)

  面向对象的语言有一个标志,他们都有类的概念,通过类可以创建多个具有相同属性和方法的对象。但是JavaScript中没有类的概念,因此JavaScript与其...

2557

扫码关注云+社区