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 条评论
登录 后参与评论

相关文章

来自专栏linux驱动个人学习

高通msm8909耳机调试

1、DTS相应修改: DTS相关代码:kernel/arch/arm/boot/dts/qcom/msm8909-qrd-skuc.dtsi: 1 s...

8175
来自专栏我和未来有约会

简练的视图模型 ViewModel

patterns & practices Developer Center 发布了 Unity Application Block 1.2 for Silver...

2349
来自专栏跟着阿笨一起玩NET

c# 使用timer定时器操作,上次定时到了以后,下次还未执行完怎么处理

------解决方案-------------------------------------------------------- 开始的时候,禁用定时器,你...

3091
来自专栏Hadoop数据仓库

Oracle sqlldr 如何导入一个日期列

1. LOAD DATA INFILE * INTO TABLE test FIELDS TERMINATED BY X'9' TRAILING NULLCO...

1876
来自专栏Ryan Miao

ehcache报错

jfinal2.0+tomcat7+ehcache2.6.11+Linux Linux version 2.6.18-164.el5 (mockbuild@x8...

3759
来自专栏linux驱动个人学习

高通Audio中ASOC的machine驱动

ASoC被分为Machine、Platform和Codec三大部分,其中的Machine驱动负责Platform和Codec之间的耦合以及部分和设备或板子特定的...

1K4
来自专栏MelonTeam专栏

Bitmap 源码阅读笔记

导语: Android 系统上的图片的处理,跟Bitmap 这个类脱不了关系,我们有必要去深入阅读里面的源码,以便在工作中能更好的处理Bitmap相关的问题...

2608
来自专栏WOLFRAM

向日葵中的数学之美

1963
来自专栏Golang语言社区

Knapsack problem algorithms for my real-life carry-on knapsack

I'm a nomad and live out of one carry-on bag. This means that the total weight o...

1192
来自专栏码匠的流水账

java9系列(五)Stack-Walking API

java9新增这个类的目的是提供一个标准API用于访问当前线程栈,之前只有Throwable::getStackTrace、Thread::getStackTr...

471

扫码关注云+社区