惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
V
V2EX
爱范儿
爱范儿
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
小众软件
小众软件
Vercel News
Vercel News
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
V
Visual Studio Blog
J
Java Code Geeks
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog
B
Blog
美团技术团队
量子位

博客园 - BAsil

ubuntu下安装git server以及eclipse egit插件配置 windows下调试android源代码 Mac OS 10.6(Snow Leopard)编译Android源代码 使用Mono Runtime Bundle制作安装包让C#桌面应用程序脱离net framework c++对象成员函数返回自身引用时出现的诡异问题及解决办法 重构一个贪吃蛇游戏(linux c++) Ubuntu 9.10 Server 配置Apache + WebDav 实现文档协同管理 javascript事件机制与jQuery.bind的补充说明 javascript 事件机制 与 jQuery.Bind Ubuntu 9.10 Server 配置基于SSL的Subversion Ubuntu 9.10 Server (Karmic) 迁移Bugzilla 域环境下配置连接sql server 的asp.net网站 注意IIS 7.0的集成管道模式下HttpApplication 请求管道事件的问题 出现Unable to launch Visual Studio's Localhost Web Server问题解决 基于TCP的网络游戏黑白棋系列(四):游戏棋盘 基于TCP的网络游戏黑白棋系列(三):游戏大厅 基于TCP的网络游戏黑白棋系列(二):数据传输 基于TCP的网络游戏黑白棋系列(一):建立连接 基于TCP的网络游戏黑白棋系列教程开篇
C++指针存储结构
BAsil · 2010-09-20 · via 博客园 - BAsil

—概述

—线性排序

—二进制存放

—数据类型和编码

—变量和指针

—引用

—数组

—动态存储分配

—函数参数传递在内存中的表示

概述

c++的学习中,对指针的理解和把握相当重要,可能很多同学对此感到困惑。我个人感觉指针的理解实际上还是依赖于对内存存储的理解,因此了解内存的存储机制就显得至关重要。

当然由于内存的存储机制实际上比较复杂,而且我们的任务是学习程序的编写,所以我这里更多是从程序的角度来看内存的存储(有一定程度的简化)。但当我们了解了内存存储的方式,知道程序中的常量、变量、指针在内存中的表示,又能够进一步指导我们程序的书写。

线性排序

首先请把内存想象成一段连续的格子,每个格子可以存储内容(如果一个格子装不下,可能放到多个格子中,这种情况很普遍)

1 

注意两点

1 格子是有序排列的,也就是每个格子都有编号,从小到大排列。我们也常说内存是线性空间排列的(就是想象成一条线)

2. 每个格子大小是1个字节(Byte 简写B) 1Byte=8bit

二进制存放

接下来我们看看1个字节都可以存放哪些内容

我们要时刻牢记内存存储的实际上只是0和1,那么就假设我们的计算机只能处理二进制数(二进制计算器?)。看看1个字节可以表示多少个数

2

上图,我们把一个字节分成8比特展开,上图表示的二进制数为 00000001,转换为十进制数为1,那么我们用一个字节最多可以表示从00000000—11111111共256个数(十进制从0到255)。

好,现在我提一个问题,如果我想表示十进制的258怎么办,因为258的二进制数为100000010,我们用一个字节无法完全容纳。
我们可能想用计算机表示一个比较大的数,比如十进制数1340054788(2009年1月我国人口数) ,如果用二进制数表示是
0100 1111 1101 1111 1001 1101 0000 0100
3 

可以看到我们需要4个字节来表示,可是计算机怎么知道它的这四个字节是表示1个数(13亿)还是4个数(1个字节1个数)亦或是3个数? 所以这里引出了数据类型的概念。

数据类型和编码

上节计算机无法确定具体的表示内容的原因是我们对不同大小内容的存放没有确切的限制,类比前面的格子,有的内容用一个格子,有的内容用两个格子,还有的用四个格子。所以必须对内容的存放做限制,比如不管多大的数统一用4个字节存放(实际上就是32位window系统整形int的长度)

4

上图是十进制的1

5

上图是十进制的4294967296 (4.2亿)

也就是说我们可以表示0~4.2亿的数,虽然这样对于小的数有空间浪费(比如十进制的1,浪费了3个字节)、对更大的数无法表示(比如5.6亿,会溢出),但是我们界定了内容的宽度,使得存储变得简单。

我们常用的整形,就是用四个字节表示,由此我们已可以推断它的取值范围(0~4.2亿,我们这里没有讨论负数,有所简化,但原理一样)。

也就是说,当我们写 int a=3; 实际上在内存中占据了4个字节,如下图

6

那么字符是怎么显示的呢

先来看一下英文字母及标点符号的表示。由于内存只能表示0、1,我们不得不采用一些办法才能存储字符。我们通常采用字符集(Character Set)的方式,即在计算机中存储时也要使用二进制数来表示,但具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫字符集,注意和编码不同)。

比如十进制的65 代表A

7

那么字符码就有26个小写字母、26个大写字母、10个数字、32个符号、33个句柄和一个空格,另外加上一些扩展字符,总共是256个字符码。同整数一样,我们也需要对字符所占的宽度进行约束(要不同样无法辨别),我们可以根据公式

Y=Log2 X Y即为所占宽度,所以256字符需要每个字符占1个字节的宽度,这也就是为什么一个字符占据一个字节的原因。

比如 char a=’3’;

char b=’A’;

这也就是我们常说的ASCII字符集(Single-Byte Character Set,简称SBCS),类似的SBCS还有阿拉伯语ISO8859-6 等等。

至于简体中文、繁体中文、日文和韩文,显然用1个字节存储是不够的,所以采用了双字节字符集的形式(Double-Byte Character Set,简称DBCS),属于DBCS的有简体中文的GB2312,

繁体中文的BIG5。

标准化协会还另外采用了2个字节的宽度来表示所有的字符的字符集形式即Unicode(宽字节字符集),旨在把全世界的文字用一种字符集(Character Set)表示。目前已经定义了包含英文(一个英文字母也占两个字节)、中文、日文、阿拉伯字母等大约35000个代码点,而实际上2个字节的宽度可以表示65536个代码点。

变量和指针

我们了解了数据类型,来看一看变量的声明和使用,以整形为例

int n;

则会在内存中的某一个位置选取4个字节存放该整形,然后计算机记录下该内存的地址(那么内存的地址如何表示?),由于声明并没有赋值,所以只是分配了内存空间并没有赋初值。

这种方式分配空间和动态分配空间(C里用malloc,C++用new)是不同的,静态方式会在该变量n超过其变量作用范围时自动释放(回收),而动态方式需要我们用(C利用free,C++用delete)回收。

如图

8

这里n可以看作是别名,我们的编译器在编译程序的时候做了一些手脚,把n和该段内存的地址建立了关联,这样后面我们可以利用n来方便的更改该段内存的内容,而实际上计算机内部处理还是按照地址去访问的。比如

n=30;

现在来了解一下地址(实际上也是我们在线性排序中提到的格子的编号),因为该编号最终也是会以二进制的形式存储,所以我们也给它规定了一个数据类型,并且限定了它的宽度,这就是指针类型,宽度为4个字节也就是常说的32位(在32位windows系统,比如xp)。

那么当我们一个格子代表一个字节而给它们进行地址编号(也就是指针)的话,最多可以表示2的32次方 4294967296个字节,实际上就是4GB。这也说明32windows系统的寻址空间最多就是4GB,window xp 最大支持的物理内存是4GB。那么如下代码

int *p;

是否像整形变量n一样需要在内存分配空间?分配多大的空间呢?它里面又存放什么内容呢?

要回答上面的问题,我们首先来看一下指针的声明

int *p;

char *x;

double *y;

上面分别声明了指向int的指针、指向char的指针、指向double的指针,它们没有本质的不同,都是占4个字节,在计算机内部处理方式是完全一样的,不同的只是编译器在编译时要检查指向int的指针必须指向int(有点啰嗦),而不能其他 。所以应该把指针理解成一个二进制数(0到4.2亿)。

再看指针的存储

int *p 声明了这样一个指针,并且像整形一样在内存里分配了四个字节的空间

9

只不过这个指针变量没有任何内容,换句话说,没有指向任何的int变量。

int n=3;

int *p;

p=&n;

其中&是取地址操作符,意思是取变量n在内存中的地址并且放到p分配的空间存放,假设是十进制的2293532,那么二进制是0000 0000 0010 0010 1111 1111 0001 1100,则p中存放的值如下图

10

提到&就不能不提* ,*是解引用操作符,那么什么时候用*呢,看一看前面定义的指针p,它里面存放的是n变量的地址,可是我们对这个二进制数不感兴趣,很多时候我们需要了解p的值(某地址)究竟存放什么样的内容?这时候就用到了*

cout <<*p<<endl;

上一句是根据地址十进制的2293532(即n对应的地址)去找里面的内容,为3。因此我们可以把p看作一个指向n的指针(指针是抽象的,实际使用地址表示,如下图)

11

如果有 int k=*p;

则会给k分配一个4个字节的空间,然后把p的内容解引用(实际上是依据p存储的内容即地址去找该地址里的内容,形象的称p指针所指向的内容)存储到k对应的空间中。

12

我们同样可以更改p的指向

比如int s=5;

P=&s;

如图

13

引用

引用有时候又称为别名,它可以用作对象的另一个名字。通过引用我们可以间接地操纵对象,使用方式类似于指针(引用在内部存放的是一个地址,这点上和指针是类似的),但是不需要指针的语法。在实际的程序中,引用主要被用作函数的形式参数或者返回值。

我们看一下引用的声明

int i=5;

int &j=I;

j=7

cout<<i<<endl;

j+=2;

cout<<i<<endl;

int *pi=&j;

cout <<pi<<endl; //打印的是i的地址

引用的所有操作实际上都是应用在它所指的对象身上,包括取地址操作符

14

如图对引用j的所有操作都作用到变量i上,使得引用j就像透明的或者不存在一样,像极了周星驰电影《大内密探零零发》中无相神王的隔山打牛重拳,“中拳者无事,旁观者毙命!”

数组

来看一下数组的声明

int ia[3];

此语句会在内存中分配12个字节的空间准备存放三个int

15

那么我们能不能得到数组元素的地址或者使用指针来操作数组元素呢,答案是肯定的。

如果只简单写ia; 即只写数组名则代表数组中第一个元素的地址,它的类型是数组元素类型的指针,本例是int *;

因此ia和&ia[0]是等价的,都是数组第一个元素的地址

int *p=&ia[0];

表示p里面存放的是数组第一个元素的地址

cout<<*p<<endl; //使用了解引用,参考整形指针解引用的处理方式,打印数组第一个元素的内容。

同理 ia+1和 &ia[1] 取到的是数组第二个元素的地址(注意ia+1中的1是增加了一个数据类型的宽度,int是4个字节)

ia[1]和*(ia+1) 一致,打印的是第二个元素的内容。

此种数组的声明方式会在离开数组变量作用域时自动释放,不会造成内存泄漏。

动态存储分配

new 该运算符返回一个指向所分配空间的指针。例如,要为一个整数动态分配存储空间,可以用下面的语句说明一个整形指针变量;

int *y;

y=new int;

*y=10;

16

为了在运行时创建一个可动态变化的一维整形数组x,可先将x声明为一个整形的指针,然后

用new为数组动态的分配存储空间。比如

int *x;

x=new int[3];

随后可以用x[0],x[1]等方式来访问每个数组元素,用下标方式访问数组元素很像静态的数组声明,比如

int x1[3];

我们可以用x1[0],x1[1]的方式来访问每个数组元素。可能同学们比较熟悉静态数组的声明访问方式,现在就需要了解动态分配存储空间的数组方式。两者有两方面的不同

1) 声明时,动态方式是声明了一个指针类型,静态数组是声明一个数组类型。但两者访问方式完全一样,较多采用x[0],x[1]等方式

2) 静态方式会在离开数组变量作用域时自动释放,而动态方式我们需要通过delete []x手动释放。

函数参数传递在内存中的表示

参数传递有两种方式:一种是按值传递, 在这种参数传递方式下,把实参的值复制到函数局部工作区相应的副本中,函数使用副本执行必要的计算。因此函数实际修改的是副本的值,实参的值不变。

按值传递有分两种:1将变量名作为形参和实参 2传递变量指针

void swap(int a,int b)

{//形参为整型变量

int temp=a;

a=b;

b=temp;

}

int main()

{

int i=3,j=4;

swap(i,j);

cout<<"i="<<i<<",j="<<j<<endl;

}

17

void swap(int *a,int *b)

{//形参为整型变量

int temp;

temp=*a;

*a=*b;

*b=temp;

}

int main()

{

int i=3,j=4;

swap(&I,&j);

cout<<"i="<<i<<",j="<<j<<endl;

}

当执行到函数体temp=*a; 内存存储如图

18

当执行到函数体*a=*b; 内存存储如图

19

当执行到函数体*b=temp; 内存存储如图

20

第二种方式是按引用传递参数。在这种参数传递方式下,需将形参声明为引用类型,即在参数名前加上符号”&”。当一个实参与一个引用类型结合时,被传递的不是实参的值,而是实参的地址。

void swap(int &a,int &b)

{//形参为整型变量

int temp;

temp=a;

a=b;

b=temp;

}

int main()

{

int i=3,j=4;

swap(i,j);

cout<<"i="<<i<<",j="<<j<<endl;

}

当执行函数体内temp=a; 内存存储如图

21

当执行函数体内a=b;内存存储如图

22

当执行函数体内b=temp;内存存储如图

23

函数的返回值同样遵循这样的两种方式(按值返回或者按引用返回),只要我们牢牢把握内存分配的实质,处理类似的问题会得心应手。

参考文献

1. C++ Primer 中文版   Stanley B.Lippman等著  潘爱民等译 中国电力出版社

2. C++ 大学教程 Harvey M.Deitel等著 邱仲潘等译 电子工业出版社

3. 数据结构(C++语言版) 王晓东著 科学出版社

4. Programming Windows(Fifth Edition)影印版 Charles Petzold  人民邮电出版社

5. Windows核心编程(第四版) Jeffery Richter著  机械工业出版社