C/C++:指针初学

整理自计蒜客-CS 112: C++ 程序设计

找不到的 是心头的悸动

指针是什么

指针是一个变量,其储存的是值的地址,而不是值本身。指针提供了另一种访问内存空间的方法:虽然我们不知道变量的名称,但我们可以通过变量存放的地址访问它。


指针的初始化

  • 可以直接初始化:
//<数据类型> *指针变量名 = 赋值;
//指针的数据类型,表示指针所指向的数据的数据类型
int a = 1;
int array[] = { 1,2,3,4,5 }; 
int *p1 = &a;
int *p2 = array; 或者 int *p2 = &array[0]

如果int *p2 = &array;将不能通过编译,原因是数据类型不匹配。

  • 也可以先定义,再赋值:
//对象指针:
Node node1;
Node *p3 = &node1;
//函数指针:
int add(int x,int y);
int (*p3)(int,int);
p3 = add;

指针的运算

  • 算数运算:
    指针可以跟 整数 进行加法和减法的运算,但是运算规则比较特殊——对指针进行加减运算的结果,与指针本身的类型密切相关。可以看出,指针+1移动了一种数据类型在内存中存放的字节数。不过,空指针和函数指针是不能进行算数运算的(无法通过编译),因为无法确定移动多少个字节。
//整数指针:
int *p1 = &a;
cout << p1 << " " << p1+1 <<endl;
//字符指针:
char c[] = "people";
char *p2 = &c;
cout << p2 << " " << p2+1 <<endl; 
printf("%p %p\n",p2,p2+1); 
//对象指针:
Node *p3 = &node;
cout << p3 << " " << p3+1 <<endl;
//函数指针:
int (*p4)(int,int) = add;
printf("%p\n",p4);
cout << p4 <<endl;

输出结果:
0x7ffe597f872c 0x7ffe597f8730 people eople 0x7ffe597f8760 0x7ffe597f8761 0x7ffe597f8750 0x7ffe597f8760 0x4009ed 1
1)以上使用cout输出字符指针的时候结果跟想象中不太一样?
答:这是因为cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。
2)为什么用cout输出函数指针会得到1呢?
答:这里先挖个坑...

  • 关系运算&逻辑运算
    指针变量的关系运算,指的是 指向相同类型数据的指针之间,进行的关系运算。 如果两个相同类型的指针相等,就表示两个指针指向的是同一个地址——不同类型的指针之间,或者指针与非 0 整数之间的比较是没有意义的。
    但是有一种情况是特殊的——指针可以跟 整数 0 之间进行比较,0专门用于表示空指针,即指针变量中保存的地址是空的,不指向任何有效的地址。
    关系运算的结果经常用于逻辑运算。
    int a = 9,b = 7;
    int *p[4] = {&a,&a,&b,NULL};
    //判断前后指针是否相等
    cout << "equal?" <<endl;
    for(int i=0;i<4;i++){
        if(p[i]==p[i+1]) {
            cout << "YES ";
        }
        else cout << "NO ";
    }
    cout <<endl;
    //判断是否为空指针
    cout << "NULL?" <<endl;
    for(int i=0;i<4;i++){
        if(p[i]==NULL) {
            cout << "YES " <<endl;
        }
        else cout << "NO ";
    }
    cout << endl;

输出结果:
equal? YES NO NO YES NULL? NO NO NO YES
值得注意的是,越界的指针数组元素p[4]是一个空指针。


空指针

为什么我们需要空指针呢?因为有的时候,我们在声明一个指针的时候,并没有一个确定的地址值可以赋给它,当程序运行到某个时刻的时候,才会将某个地址赋值给这个指针。这样,在指针定义但没有使用的这段时间里,它的值是不确定的——要是误用了这个不确定的指针的话,就很有可能会造成不可预见的错误(比如意外地把某个不该变更的值给改掉了),因此在这种情况下,我们首先应该将地址设置为空。

除了给指针赋值0NULL使其为空,在C++11标准中,我们还可以使用nullptr关键字来表示空指针,用法跟NULL基本相同(需要引用命名空间std中的对应标识符)。


指针与数组

数组的本质,实际上是一串连续的相同大小的内存空间——比如说,对于整形数组int a[10],它在内存中就是连续排列的十个可以容纳一个整形变量的内存空间。而数组的名称,其实就是一个常量指针,即不能被赋值的指针。作为一个指针,数组名指向的是数组的第一个元素。

指针加减运算的特点,使得它可以特别被用于处理存储在一段连续内存空间中的同类数据。而数组正好是具有一定顺序关系的,若干同类型变量的集合体——数组元素的存储,在物理上与逻辑上都是连续的,数组名就是变量的首地址。如果有数组array[5],那么array&array[0]是相同的。

  • 要访问数组元素,下面两种方法是等效的:
int *p = array;
cout << array[10] <<endl;
cout << *(p+10) <<endl;
  • 此外,如果我们要把数组作为函数的形参的话,那么它实际上是等价于把指向数组元素类型的指针作为形参的——例如,下面三个写法,出现在形参列表中就是等价的:
void f(int p[]);
void f(int p[3]);
void f(int *p);

指针数组

如果一个数组的所有元素都是指针变量,那么这就是一个指针数组。指针数组的每一个元素都必须是同一类型的指针。指针数组有一个神奇的应用:

//创建一个指针数组,其元素分别指向三个数组
int line1[]={1,0,0};
int line2[]={0,1,0};
int line3[]={0,0,1};
int *pLine[3]={line1,line2,line3};

//用类似二维数组的形式访问三个数组
for(int i=0;i<3;i++){
    for(int j=0;j<3;j++){
      cout << pLine[i][j] << “ ”;
    }
}

输出结果:
1 0 0 0 1 0 0 0 1

上个例子中的pLine在使用上跟一个二维数组没有区别,但是在存储方式上,它跟真正的二维数组并不相同:

二维数组在内存中,是以行优先的方式按照一维顺序关系存放的。因此,对于二维数组,可以将其理解成一个一维数组的一维数组,其首地址为数组名,元素个数就是行数——而它的每一个元素,就是一个一维数组。

然而,对于指针数组pLine,它的三个“元素数组”在内存中,并不是连续存放的——访问line2或者line3的时候,首先要在pLine中找出对应的元素指针,即为指向line2或者line3头元素的地址,然后再通过指针跳转到要访问的数组。

使用指针数组的情形

对象指针

跟基本类型的变量一样,每一个对象在初始化之后,都会在内存中占据一定的空间——所以我们同样也可以通过地址来访问一个对象。尽管对象同时包含了数据和函数两种成员,但是对象所占据的内存空间只用于存放数据成员——函数成员并不在每一个对象的存储副本之中。对象指针就是用于存放对象地址的变量——对象指针遵循一般变量指针的各种规则。

  • 通过对象名,我们可以访问对象成员——同样,通过对象指针,我们可以访问对象的成员,以下三种方法完全等价:
//假设已有Line类
cout << line1.getLength() <<endl;
cout << line_ptr->getLength() <<endl;
cout << (*line_ptr).getLength() <<endl;
  • this指针
    对于类的成员函数来说,我们可以直接在函数体内访问成员变量——例如,如果对象Line有一个成员变量length的话,那么我们就可以直接在成员函数内访问这个成员:
int getLength(){return length;}

而实际上,C++ 为每一个类的非静态成员函数(就是没有static关键字的成员函数),都提供了一个隐含的指针this,当我们写下return length;的时候,编译器执行的实际上是return this->length;
this指针明确地指出了函数当前所操作的数据所属的对象——它是成员函数隐藏的一个形参,当我们在成员函数中操作对象的数据成员的时候,我们其实就是在使用this指针。
然在一般情况下,我们不需要特别把this指针写出来——但是如果函数的形参列表中的参数跟成员变量重名的话,那么由于标识符作用域覆盖,我们将没法直接通过成员变量名来访问它。当然我们也可以选择更改形参名——但是更好的办法是通过this指针来访问成员变量,这样我们可以让代码拥有更好的可读性:

void setLength(int length){
    this->length=length;
}

另一种解决方法是使用初始化列表:

void setLength(int length):length(length){
}

函数指针

以上我们使用的指针都是指向数据的——而在程序运行的时候,不仅数据要占据内存空间,执行程序的代码也会被存入到内存,并占据一定的空间。每一个函数都有函数名,而实际上这个函数名就表示函数的代码在内存中的起始地址。在程序中可以像使用函数名一样,使用指向函数的指针来调用函数——也就是说,一旦函数指针指向了某个函数,那么它与函数名就具有同样的作用。

函数名在表示函数代码起始地址的同事,也包括函数的返回值类型,以及参数的个数、类型、排列次序等信息。因此,在通过函数名调用函数的时候,编译器就可以自动检查实参与形参是否相符,用函数的返回值参与其他运算时,能够自动进行类型一致性检查。而函数指针也具有同样的效果。

  • 声明:
    声明一个函数指针时,需要提供构造一个函数需要的所有信息——包括函数的返回值和形式参数列表,如下所示:
返回值类型 (* 函数指针名)(形参表)

由于对函数指针的定义在形式上比较复杂,如果在程序中出现多个这样的定义,那么多次重复这样的定义会相当繁琐。这里我们有一种很方便的解决方案——使用typedef。例如:

typedef int (* DoubleIntFunction)(double);

这里我们声明了DoubleIntFunction为“有一个double形参,返回类型为int的函数的指针”的类型的别名——接下来,如果我们需要声明这个类型的变量的时候,我们就可以直接进行使用:

DoubleIntFunction funcPtr;

这样我们就可以直接使用这个类型的指针funcPtr了。

  • 赋值:
函数指针名=函数名;

注意这里的“函数名”必须是一个已经声明过的函数,并且必须具有跟函数指针相同返回类型跟相同参数表的函数。赋值之后你就可以像使用函数一样,使用函数指针了。

  • 使用:
int add(int x,int y) {
    return x+y;
}
int (*func_ptr)(int,int);
func_ptr = add;
//以下二者完全等价
cout << func_ptr(2,3) <<endl;
cout << add(2,3) <<endl;
  • C++ 11 提供的lambda 表达式,它可以替代函数指针的作用。

动态内存分配

动态内存解决了诸如“用户输入XX个数据,那么我应该开多大的数组?”之类的只能在程序运行时才能确定的问题。那跟指针有什么关系呢?这是因为我们申请的动态内存时,返回的就是指向这个这个动态内存首地址的指针。

在 C++ 中,动态内存分配可以保证程序在运行的过程中,可以按照实际需要申请适量的内存,等到使用结束之后我们还可以将其释放——这种在程序运行的过程中申请和释放的存储单元也称为堆对象,而动态内存分配所调用的内存空间则称为堆内存。建立和删除堆对象使用以下两个运算符:newdelete

  • new的功能是动态分配内存,其语法形式如下所示:
new 数据类型(初始化参数列表);

以上语句的作用是在程序运行的过程中,申请分配用于存放指定类型数据的内存空间,然后根据参数列表中给出的值来进行初始化。如果内存申请成功,那么new运算符就会返回一个指向新分配内存区域首地址的指针——我们可以通过这个指针来访问堆对象。

  • 例如我们申请一个int类型的内存空间:
int *point;
point=new int(2);

以上,系统动态分配了用于存放int类型数据的内存空间,然后用初始值2赋值,得到的地址返回给point指针变量。我们也可以这么写,但注意区别:

int *point = new int;//没有初值
int *point = new int();//初始值为0
  • 还可以解决“开多大数组?”的问题:
cin >> n;
int *a=new int[n];

其中,方括号内的表达式表示数组长度,它可以是任何能够得到正整数值的式子。

  • 除了数组类型跟基本类型之外,new运算符还可以建立一个类的实例对象:
//假设已有类Node
Node *node_ptr;
node_ptr = new Node();

如果要建立一个对象的话,那么这里的“参数列表”就要跟对象所属类的构造函数一一对应:如果不写括号或者括号里为空的话,那么就会调用类的默认构造函数;而如果写了对应的参数的话就会调用类所具有的对应的构造函数

  • delete的作用是删除一个用new建立的对象,回收其申请的内存。
    所有用new分配的内存,都必须使用delete进行回收,否则会导致动态分配的内存无法回收,造成内存泄露!另外,deletenew是一一对应的,不能delete一个不是用new建立的对象,否则会出现“段错误”之类的问题。
    使用方法比较简单——如果你觉得一个堆对象已经不再被需要,那么你直接将其删除即可,如下所示:
delete node_ptr;//对于基本类型或者对象的指针
delete[] array_ptr;//对于指向数组的指针

注意如果要删除的是一个数组的话,那么后面的那对方括号不可省略

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 题目类型 a.C++与C差异(1-18) 1.C和C++中struct有什么区别? C没有Protection行为...
    阿面a阅读 7,627评论 0 10
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,504评论 1 51
  • 基本概念 1a general-purpose programming language用于创建计算机程序。艺术类...
    伍帆阅读 1,303评论 0 1
  • C++运算符重载-下篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 1,430评论 0 49
  • 这是一个腾讯网上的几条话,觉得很有道理,就想着记下来,或许会有用呢。 1、开始和你欣赏的人在一起;(这个道理我是赞...
    泪言丫头阅读 268评论 0 0