2006年07月25日

相信很多人都对网络编程感兴趣,下面我们就来介绍,在网络编程
中应用最广泛的编程接口Winsock API.

使用Winsock API的编程,应该了解一些TCP/IP的基础知识.虽然你可
以直接使用Winsock API来写网络应用程序,但是,要写出优秀的网络
应用程序,还是必须对TCP/IP协议有一些了解的.

1. TCP/IP协议与Winsock网络编程接口的关系.

在开始之前,我们先说一下Winsock和TCP/IP到底是什么关系.

我碰到很多人问我:怎样使用Winsock协议编程? 其实,这话说的有点
错误,Winsock并不是一种网络协议,他只是一个网络编程接口,也就
是说,他不是协议,但是他可以访问很多种网络协议,你可以把他当作
一些协议的封装.现在的Winsock已经基本上实现了与协议无关.你可
以使用Winsock来调用多种协议的功能.

那么,Winsock和TCP/IP协议到底是什么关系呢?实际上,Winsock就是
tcp/ip协议的一种封装,你可以通过调用winsock的接口函数来调用
tcp/ip的各种功能.例如我想用Tcp/ip协议发送数据,你就可以使用
winsock的接口函数send()来调用tcp/ip的发送数据功能,至于具体
怎么发送数据,Winsock已经帮你封装好了这种功能.

2.TCP/IP协议介绍

现在来介绍一些tcp/ip的原理.tcp/ip协议包含的范围非常的广,他
是一种四层协议,包含了各种,硬件软件需求的定义,我们这里只介绍
软件方面的知识.tcp/ip协议确切的说法应该是tcp/udp/ip协议.

udp协议(User Datagram Protocol 用户数据报协议).是一种保护消
息边界的,不保障可靠数据的传输.
tcp协议(Transmission Control Protocol 传输控制协议).是一种
流传输的协议.他提供可靠的,有序的,双向的,面向连接的传输.

3.保护消息边界和流

那么什么是保护消息边界和流呢?

保护消息边界,就是指传输协议把数据当作一条独立的消息在网上
传输,接收端只能接收独立的消息.也就是说存在保护消息边界,接收
端一次只能接收发送端发出的一个数据包.
而面向流则是指无保护消息保护边界的,如果发送端连续发送数据,
接收端有可能在一次接收动作中,会接收两个或者更多的数据包.

我们举个例子来说,例如,我们连续发送三个数据包,大小分别是2k,
4k , 8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使
用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有
三次接收动作,才能够把所有的数据包接收完.而使用TCP协议,我们
只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的
数据包接收下来.只需要有一次接收动作.

这就是因为UDP协议的保护消息边界使得每一个消息都是独立的.而
流传输,却把数据当作一串数据流,他不认为数据是一个一个的消息.

所以有很多人在使用tcp协议通讯的时候,并不清楚tcp是基于流的
传输,当连续发送数据的时候,他们时常会认识tcp会丢包.其实不然,
因为当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚
至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个
数据包,而已经接收的其他数据包却被忽略了.所以大家如果要作这
类的网络编程的时候,必须要注意这一点.

4。Winsock编程简单流程

下面我们介绍一下Win32平台的Winsock编程方法.
通讯则必须有服务器端,和客户端.
我们简单介绍tcp服务器端的大体流程.

对于任何基于Winsock的编程首先我们必须要初始化Winsock DLL库.
int WSAStarup( WORD wVersionRequested , LPWSADATA lpWsAData ).
wVersionRequested是我们要求使用的Winsock的版本.
调用这个接口函数可以帮我们初始化Winsock .然后我们必须创建一
个套接字(socket).
SOCKET socket( int af , int type , int protocol );
套接字可以说是Winsock通讯的核心.Winsock通讯的所有数据传输,
都是通过套接字来完成的,套接字包含了两个信息,一个是IP地址,一
个是Port端口号,使用这两个信息,我们就可以确定网络中的任何一
个通讯节点.

当我们调用了socket()接口函数创建了一个套接字后,我们必须把套
接字与你需要进行通讯的地址建立联系,我们可以通过绑定函数来实
现这种联系.
int bind(SOCKET s , const struct sockaddr FAR* name , int namelen ) ;
struct sockaddr_in
{
short sin_family ;
u_short sin_prot ;
struct in_addr sin_addr ;
char sin_sero[8] ;
}
就包含了我们需要建立连接的本地的地址,包括,地址族,ip和端口信
息.sin_family字段我们必须把他设为AF_INET,这是告诉Winsock使
用的是IP地址族.sin_prot 就是我们要用来通讯的端口号.sin_addr
就是我们要用来通讯的ip地址信息.

在这里,必须还得提一下有关’大头(big-endian)’小头(little-endian)’.
因为各种不同的计算机处理数据时的方法是不一样的,Intel 86处理
器上是用’小头’形势来表示多字节的编号,就是把低字节放在前面,
把高字节放在后面,而互联网标准却正好相反,所以,我们必须把主机
字节转换成网络字节的顺序.Winsock API提供了几个函数.

把主机字节转化成网络字节的函数;
u_long htonl( u_long hostlong );
u_short htons( u_short hostshort );
把网络字节转化成主机字节的函数;
u_long ntohl( u_long netlong ) ;
u_short ntohs( u_short netshort ) ;
这样,我们设置ip地址,和port端口时,就必须把主机字节转化成网络
字节后,才能用bind()函数来绑定套接字和地址.

当绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的
连接请求.
int listen( SOCKET s ,int backlog );
这个函数可以让我们把套接字转成监听模式.
如果客户端有了连接请求,我们还必须使用
int accept( SOCKET s , struct sockaddr FAR* addr , int FAR* addrlen );
来接受客户端的请求.
现在我们基本上已经完成了一个服务器的建立,
而客户端的建立的流程则是初始化WinSock ,然后创建socket套接字
,再使用
int connect( SOCKET s , const struct sockaddr FAR* name , int namelen ) ;
来连接服务端.

下面是一个最简单的创建服务器端和客户端的例子:
服务器端的创建 :
WSADATA wsd ;
SOCKET sListen ;
SOCKET sclient ;
UINT port = 800 ;
int iAddrSize ;
struct sockaddr_in local , client ;
WSAStartup( 0×11 , &wsd );
sListen = socket ( AF_INET , SOCK_STREAM , IPPOTO_IP ) ;
local.sin_family = AF_INET ;
local.sin_addr = htonl( INADDR_ANY ) ;
local.sin_port = htons( port ) ;
bind( sListen , (struct sockaddr*)&local , sizeof( local ) ) ;
listen( sListen , 5 ) ;
sClient = accept( sListen , (struct sockaddr*)&client , &iAddrSize ) ;

客户端的创建:
WSADATA wsd ;
SOCKET sClient ;
UINT port = 800 ;
char szIp[] = "127.0.0.1" ;
int iAddrSize ;
struct sockaddr_in server ;
WSAStartup( 0×11 , &wsd );
sClient = socket ( AF_INET , SOCK_STREAM , IPPOTO_IP ) ;
server.sin_family = AF_INET ;
server.sin_addr = inet_addr( szIp ) ;
server.sin_port = htons( port );
connect( sClient , (struct sockaddr*)&server , sizeof( server ) ) ;

当服务器端和客户端建立连接以后,无论是客户端,还是服务器端都
可以使用
int send( SOCKET s , const char FAR* buf , int len , int flags );
int recv( SOCKET s , char FAR* buf , int len , int flags );
函数来接收和发送数据,因为,TCP连接是双向的.
当要关闭通讯连结的时候,任何一方都可以调用
int shutdown( SOCKET s , int how ) ;
来关闭套接字的指定功能。再调用
int closesocket( SOCKET s) ;
来关闭套接字句柄。这样一个通讯过程就算完成了。

注意:上面的代码没有任何检查函数返回值,如果你作网络编程就一定要
检查任何一个Winsock API函数的调用结果,因为很多时候函数调用
并不一定成功.上面介绍的函数,返回值类型是int的话,如果函数调
用失败的话,返回的都是SOCKET_ERROR.

5。Winsock编程的五种模型

上面介绍的仅仅是最简单的winsock通讯的方法,而实际中很多网络
通讯的却很多难以解决的意外情况.

例如,Winsock提供了两种套接字模式:锁定和非锁定.当我们使用锁
定套接字的时候,我们使用的很多函数,例如accpet,send,recv等等,
如果没有数据需要处理,这些函数都不会返回,也就是说,你的应用程
序会阻塞在那些函数的调用处.而 如果使用非阻塞模式,调用这些函
数,不管你有没有数据到达,他都会返回,所以,有可能我们在非阻塞
模式里,调用这些函数大部分的情况下会返回失败,所以就需要我们
来处理很多的意外出错.

这显然不是我们想要看到的情况.我们可以采用Winsock的通讯模型
来避免这些情况的发生。

Winsock提供了五种套接字I/O模型来解决这些问题.他们分别是
select(选择),WSAAsyncSelect(异步选择),
WSAEventSelect (事件选择), overlapped(重叠) , completion
port(完成端口) .

我们在这里详细介绍一下select,WSAASyncSelect两种模型.

select模型是最常见的I/O模型.
使用
int select( int nfds , fd_set FAR* readfds , fd_set FAR* writefds , fd_set FAR* exceptfds ,
const struct timeval FAR * timeout ) ;
函数来检查你要调用的socket套接字是否已经有了需要处理的数据.
select包含三个socket队列,分别代表:
readfds ,检查可读性,writefds,检查可写性,exceptfds,例外数据.
timeout是select函数的返回时间.
例如,我们想要检查一个套接字是否有数据需要接收,我们可以把套
接字句柄加入可读性检查队列中,然后调用select,如果,该套接字没
有数据需要接收,select函数会把该套接字从可读性检查队列中删除
掉,所以我们只要检查该套接字句柄是否还存在于可读性队列中,就
可以知道到底有没有数据需要接收了.

Winsock提供了一些宏用来操作套接字队列fd_set.
FD_CLR( s,*set) 从队列set删除句柄s.
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中.
FD_SET( s,*set )把句柄s添加到队列set中.
FD_ZERO( *set ) 把set队列初始化成空队列.

WSAAsyncSelect(异步选择)模型:
WSAASyncSelect模型就是把一个窗口和套接字句柄建立起连接,套接
字的网络事件发生时时候,就会把某个消息发送到窗口,然后可以在
窗口的消息响应函数中处理数据的接收和发送.
int WSAAsyncSelect( SOCKET s, HWND hWnd , unsigned int wMsg , long lEvent ) ;
这个函数可以把套接字句柄和窗口建立起连接,
wMsg 是我们必须自定义的一个消息.
lEvent就是制定的网络事件.包括FD_READ , FD_WRITE , FD_ACCEPT
, FD_CONNECT , FD_CLOSE .
几个事件.
例如,我需要接收FD_READ , FD_WRITE , FD_CLOSE 的网络事件.可
以调用
WSAAsyncSelect( s , hWnd , WM_SOCKET , FD_READ | FD_WRITE | FD_CLOSE ) ;
这样,当有FD_READ , FD_WRITE 或者 FD_CLOSE网络事件时,窗口
hWnd将会收到WM_SOCKET消息,消息参数的lParam标志了是什么事件
发生.

其实大家应该见过这个模型,因为MFC的CSocket类,就是使用这个模
型.

上面为大家介绍了Winsock编程的一些方法.

 

在ASP中日期比较使用:DateDiff( "d", "2006-1-30", now )>0;在C#中使用:TimeSpan

代码如下:
using System;
using System.Collections;

public class DatediffClass
{
 public static void Main()
 {
  DateTime dt1 = DateTime.Parse("2006-04-01");
  DateTime dt2 = DateTime.Parse("2006-05-01");

  TimeSpan ts = dt2.Subtract(dt1);
  Console.WriteLine(ts.TotalDays);
  Console.ReadLine();
 }

}

如果是比较大小:DateTime.Compare(t1, t2) >  0 或者 t1.CompareTo(t2) > 0

2006年06月20日

20.4 delete/delete[] 的两个注意点

指针通过 new new[] ,向系统“申请”得到一段内存空间,我们说过,这段内存空间必须在不需要将它释放了。有点像人类社会的终极目标“共产主义”下的“按需分配”。需要了就申请,不需要了,则主动归还。

 

现在问题就在于这个“主动归还”。当然,指针并不存在什么思想觉悟方面的问题,说光想申请不想归还。真正的问题是,指针在某些方面的表现似乎有些像“花心大萝卜”。请看下面代码,演示令人心酸的一幕。

 

/*

  初始化 p  —– p 的新婚

  通过 new ,将一段新建的内存“嫁给”指针p

  这一段分配的内存,就是p的原配夫妻

*/

int* p = new int[100]; 

 

 

/*

   使用 p  —– 恩爱相处

   N 多年恩爱相处,此处略去不表

*/

……

 

/*

   p 改变指向 —- 分手

*/

 

int girl [100];   //第三者出现

p = girl;         //p 就这样指向 girl

 

 

/*

   delete [] p —-  落幕前的灾难 

 

   终于有一天,p老了,上帝选择在这一时刻

   惩罚他

*/

 

delete [] p;

 

扣除注释,上面只有4行代码。这4行代码完全符合程序世界的宪法:语法。也就是说对它们进行编译,编译器会认为它们毫无错误,轻松放行。

 

但在灾难在 delete [] p 时发生。

我们原意是要释放 p 最初通过 new int[100]而得到的内存空间,但事实上,p那时已经指向girl[100]了。结果,第一、最初的空间并没有被释放。第二、girl[100] 本由系统自行释放,现在我们却要强行释放它。

 

20.4.1 一个指针被删除时,应指向最初的地址

 

当一个指针通过 +,- 等操作而改变了指向;那么在释放之前,应确保其回到原来的指向。

 

比如:

 

int* p = new int[3];

 

*p = 1;

cout << *p << endl;

 

p++;    //p的指向改变了,指向了下一元素

*p = 2;

cout << *p << endl;

 

//错误的释放:

delete [] p;

 

delete [] p 时,p指向的是第二个元素,结果该释放将产生错位:第一个元素没有被释放,而在最后多删除了一个元素。相当你盖房时盖的是前3间,可以在拆房时,漏了头一间,从第二间开始拆起,结果把不是你盖的第4房间倒给一并拆了。

 

如何消除这一严重错误呢?

第一种方法是把指针正确地""回原始位置:

 

p–;

delete [] p;

 

但当我们的指针指向变化很多次时,在释放前要保证一步不错地一一退回,会比较困难。所以另一方法是在最初时“备份”一份。在释放时,直接释放该指针即可。

 

int* p = new int[3];

int* pbak = *p;    //备份

 

//移动 p

……

 

//释放:

delete [] pbak;

 

由于pbak正是指向p最初分配后的地址,我们删除pbak,就是删除p最初的指向。此时我们不能再删除一次p。这也就引出new / delete new[] / delete[] 在本章的最后一个问题。

 

20.4.2 已释放的空间,不可重复释放

 

第一种情况,错了最直接:

 

int* p = new int(71);

cout << *p << endl;

 

delete p; //OK!

delete p; //ERROR! 重复删除p

 

当然,如果同一指针在delete之后,又通过new new[] 分配了一次内存,则需要再删除一次:

 

int* p = new int(71);

cout << *p << endl;

 

delete p; //OK!

p = new int(81);

delete p; //OK!

 

p = new int[10];

for (int i=0; i<10; i++)

  *p = i;

 

delete [] p; //OK!

 

上面代码中,共计三次对p进行delete delete[],但不属于重复删除。因为每次delete都对应一次新的new

我们下面所说的例子,均指一次delete之后,没有再次new,而重复进行delete

 

第二种情况,重复删除同一指向的多个指针

 

int* p1 = new int(71);

int* p2 = p1;   //p2和p1 现在指向同一内存地址

 

cout << *p1 << endl;

cout << *p2 << endl;

 

delete p1;  //OK

delete p2;  //ERROR! p2所指的内存,已通过delete p1而被释放,不可再delete一次。

 

同样的问题,如果你先删除了p2,则同样不可再删除p1。

 

delete p2; //OK

delete p1; //ERROR

 

第三种情况,删除指向某一普通变量的指针

 

int a = 100;

int* p = &a;

delete p;  //ERROR

 

p 不是通过new 得到新的内存空间,而是直接指向固定变量:a。所以删除p等同要强行剥夺a的固有空间,会导致出错。

 

20.5 C 方式的内存管理

 

new/delete只在C++里得到支持。在C里,内存管理是通过专门的函数来实现。另外,为了兼容各种编程语言,操作系统提供的接口通常是 C 语言写成的函数声明 (Windows 本身也由C和汇编语言写成)。这样,我们就不得不同时学习C的内存管理函数。

 

20.5.1 分配内存 malloc 函数

 

需要包含头文件:

#include <alloc.h>

#include <stdlib.h>

 

函数声明(函数原型)

void *malloc(int size);

 

说明:malloc 向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。

 

从函数声明上可以看出。malloc new 至少有两个不同: new 返回指定类型的指针,并且可以自动计算所需要大小。比如:

 

int *p;

 

p = new int; //返回类型为int* 类型(整数型指针),分配大小为 sizeof(int);

或:

 

int* parr;

 

parr = new int [100];  //返回类型为 int* 类型(整数型指针),分配大小为 sizeof(int) * 100;

 

malloc 则必须由我们计算要字节数,并且在返回后强行转换为实际类型的指针。

 

int* p;

 

p = (int *)  malloc (sizeof(int));

 

第一、malloc 函数返回的是 void * 类型,如果你写成:p = malloc (sizeof(int)); 则程序无法通过编译,报错:“不能将 void* 赋值给 int * 类型变量”。所以必须通过 (int *) 来将强制转换。

第二、函数的实参为 sizeof(int) ,用于指明一个整型数据需要的大小。如果你写成:

 

int* p = (int *) malloc (1);

代码也能通过编译,但事实上只分配了1个字节大小的内存空间,当你往里头存入一个整数,就会有3个字节无家可归,而直接“住进邻居家”!造成的结果是后面的内存中原有数据内容全部被清空。

 

malloc 也可以达到 new [] 的效果,申请出一段连续的内存,方法无非是指定你所需要内存大小。

 

比如想分配100个int类型的空间:

 

int* p = (int *) malloc ( sizeof(int) * 100 ); //分配可以放得下100个整数的内存空间。

 

另外有一点不能直接看出的区别是,malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。

 

除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。

 

20.5.2 释放内存 free 函数

 

需要包含头文件( malloc 一样)

#include <alloc.h>

#include <stdlib.h>

 

函数声明:

void free(void *block);

 

即: void free(指针变量)

之所以把形参中的指针声明为 void* ,是因为free必须可以释放任意类型的指针,而任意类型的指针都可以转换为void *

 

举例:

 

int* p = (int *) malloc(4); 

 

*p = 100;

 

free(p); //释放 p 所指的内存空间

 

或者:

 

int* p = (int *) malloc ( sizeof(int) * 100 ); //分配可以放得下100个整数的内存空间。

 

……

 

free(p);

 

free 不管你的指针指向多大的空间,均可以正确地进行释放,这一点释放比 delete/delete [] 要方便。不过,必须注意,如果你在分配指针时,用的是newnew[],那么抱歉,当你在释放内存时,你并不能图方便而使用free来释放。反过来,你用malloc 分配的内存,也不能用delete/delete[] 来释放。一句话,new/deletenew[]/delete[]malloc/free 三对均需配套使用,不可混用!

 

int* p = new int[100];

… …

free(p);  //ERROR! p 是由new 所得。

 

这也是我们必须学习 malloc free 的重要理由之一,有时候,我们调用操作系统的函数(Windows API)时,会遇到由我们的程序来分配内存,API函数来释放内存;或API函数来分配内存,而我们的程序来负责释放,这时,必须用mallocfree来进行相应的工作。

 

当然,保证所说的内存分配与释放方式不匹配的错误发生,Windows API函数也提供了一套专门的内存管理函数给程序员,为了不在这一章里放太多相混的内容,我们在Windows编程的课程再讲相关内容。

 

最后还有一个函数,也是我们要学习C方式的内存管理函数的原因。

 

20.5.3 调空间的大小realloc 函数

 

需要包含头文件( malloc 一样)

#include <alloc.h>

#include <stdlib.h>

 

函数声明:

void *realloc(void *block, int size);

 

block 是指向要扩张或缩小的内存空间的指针。size 指定新的大小。

 

realloc 可以对给定的指针所指的空间进行扩大或者缩小。size 是新的目标大小。比如,原来空间大小是40个字节,现在可以将size 指定为60,这样就扩张了20个字节;或者,将size 指定为20,则等于将空间缩小了20个字节。

 

无论是扩张或是缩小,原有内存的中内容将保持不变。当然,对于缩小,则被缩小的那一部分的内容会丢失。

 

举例:

 

//先用 malloc 分配一指针

int* p = (int *) malloc (sizeof(int) * 10);  //可以存放10个整数

 

……

//现在,由于些某原因,我们需要向p所指的空间中存放15个整数

//原来的空间不够了:

p = (int *) realloc (p, sizeof(int) *15); //空间扩张了 (15 – 10) * sizeof(int) = 20 个字节

 

……

 

//接下来,我们决定将p所指内存空间紧缩为5个整数的大小:

p = (int *) realloc (p, sizeof(int) * 5); //缩小了 (15 – 5) * sizeof(int) = 40 个字节

 

……

 

free (p);

 

这么看起来,realloc 有点像是施工队对一个已建的房屋进行改修:可以将房间后面再扩建几间,也可以拆掉几间。不管是扩还是拆,屋里原来的东西并不改变。

不过,这里要特别提醒一点:这个施工队有时会做这种事:1、在一块新的空地上新建一座指定大小的房屋;2、接着,将原来屋子里的东西原样照搬到新屋;3、拆掉原来的屋子。

这是什么指意呢?

realloc 并不保证调整后的内存空间和原来的内存空间保持同一内存地址。相反,realloc 返回的指针很可能指向一个新的地址。

所以,在代码中,我们必须将realloc返回的值,重新赋值给 p :

 

p = (int *) realloc (p, sizeof(int) *15);

 

 

甚至,你可以传一个空指针(0)给 realloc ,则此时realloc 作用完全相当于malloc

 

int* p = (int *) realloc (0,sizeof(int) * 10);  //分配一个全新的内存空间,

 

这一行,作用完全等同于:

 

int* p = (int *) malloc(sizeof(int) * 10);

 

20.5.4 mallocreallocfree的例子

 

打开CB6,新建一空白控制台工程。

 

第一步:在 Unit1.cpp 中的最前面,加入引用 alloc.h 等头文件的代码:

 

……

#pragma hdrstop

#include <alloc.h> //三个函数的声明都这个头文件里

#include <iostream.h>

……

 

第二步:将以下代码加入主函数 main 中间:

 

int* p = (int *) malloc (sizeof(int) * 10);

 

cout << "realloc 之前p指向的内存地址: " << p << endl;

 

for (int i=0; i<10; i++)

{

   p[i] = i + 1;

}

 

cout << "realloc 之前p指向的内存中的内容:" << endl;

 

for (int i=0; i<10; i++)

{

   cout << p[i] << ",";

}

cout << endl;

 

p = (int *) realloc (p, sizeof(int) * 15);

 

cout << "realloc 之后p指向的内存地址: " << p << endl;

 

cout << "realloc 之后p指向的内存中的内容:" << endl;

for (int i=0; i<15; i++)

{

   cout << p[i] << ",";

}

cout << endl;

 

free (p);

system("PAUSE");

 

 

运行结果:

(malloc, realloc, free 上机)

 

从图中我们看到 realloc 前后的p指向的内存地址同样是 9647936 。但记住,这并不是结论,真正的结论我们已经说过“realloc 并不保证返回和原来一样的地址。”所谓的“并不保证”的意思是:“我尽力去做了,但仍然有可能做不到。”。

 

无论调用几次 realloc,最后我们只需一次 free

20.2.5 new delete 的关系

 

如果只有“建房”而没有“拆房”,那么程序就会占用内存越来越多。所以,当使用new 为某个指针分配出内存空间后,一定要记得在不需要再使用时,用delete 删除。下面是一个例子。演示new delete 的对应使用。

 

//建屋和入住:

1) int* p = new int(100);

 

//使用:

2) cout << *p << endl;

 

//拆:

3) delete p;

 

看,第1句,申请了4字节的内存空间,同时存入值为100的整数。

第2句,在屏幕上输出入住者的值 (100)

第3句,释放内存(这4字节被系统收回准备做其它用途)。入住者呢?自然消失了。

 

前面举的例子都是在 new 一个 int 类型,其它类型也一样:

 

char* a = new char(‘A’);

cout << *a << endl;

*a = ‘B’;

cout << *a << end;

delete a;

 

bool* b = new bool;

*b = true;

 

if (*b)

   cout << "true" << endl;

else

  cout << "fale" << endl;

 

但是这些都是简单数据类型,如果要分配数组一样的连续空间,则需要使另一对武器。

 

20.3 new [] delete []

 

new / delete 用于分配和释放单个变量的空间,而 new [] / delete[] 则用于分配连续多个变量的存间。

 

20.3.1 new[] / delete[] 基本用法

 

new [] 语法:

 

指针变量 = new 数据类型[元素个数]

 

语法实例:

 

int* p = new int[20];

 

首先,你需要迅速回想一下,如果是 int* p = new int(20); 那么该是什么作用?否则你很容易在事后把二者混了。

 

实例中,用 new 申请分配了20个连续的整数所需的空间,即:20 * sizeof(int) = 80个字节。

图示为:

(指针变量p指向一段连续的内存空间)

 

new int 只是分配了一个整数的内存空间,而 new int[N]却分配了N个整数的连续空间。看来,new[] new “威力更猛”,所以,我们同样得记得:用 new [] 分配出空间,当不在需要时,必须及时调用 delete [] 来释放。

 

delete [] 语法:

 

delete [] 指针变量;

 

如:

 

//分配了可以存放1000个int的连续内存空间:

int* p = new int[1000]; 

 

//然后使用这些空间:

……

 

//最后不需要了,及时释放:

delete [] p;

 

20.3.2 new []/ delete[] 示例

 

Windows XP Windows NT Windows 2000中,按 Ctrl + Alt + Del (其它操作系统,如Windows98/Me等千万不要按些组合键,否则电脑将重启)。可以调出 Windows 任务管理器,其中要以看出当前粗略的的内存使用量。下面我们结合该工具,写一个程序,先分配100M的内存,再释放。

 

这是程序代码的抓图:

 

各步运行结果:

程序显示 任务管理器抓图

第一步:分配内存之前

(任务管理显示我的机器使用了207兆的内存)

第二步:分配了100兆的内存

(多出了100M)

第三步:又释放出这100兆

(回到207兆)

 

注意:使用 new 得来的空间,必须用 delete 来释放;使用 new [] 得来的空间,必须用 delete [] 来释放。彼此之间不能混用。

 

new [] 分配出连续空间后,指针变量“指向”该空间的首地址。

 

20.3.3 详解指向连续空间的指针

 

通过 new [] 指向连续空间以后,p 就变得和一个一维数组很是类似。我们先来复习一下数组相关知识。

 

假设是这么一个数组:

 

int arr[20];

 

arr 的内存示意图为(为了不太占用版面我缩小了一点):

 

(数组 arr 的内存示意)

 

和指针变量相比, 数组没有一个单独的内存空间而存放其内存地址。即:指针变量p是一个独立的变量,只不过它的值指向另一段连续的内存空间;而数组arr,本身代表的就是一段连续空间。

 

如果拿房间来比喻。指针和数组都是存放地址。只不过,指针是你口袋里的那本通讯录上写着的地址,你可以随时改变它的内容,甚至擦除。而数组是你家门楣上钉着的地址,你家原来是“复兴路甲108号”,你绝对不能趁月黑天高,把它涂改为“唐宁街10号”。

 

数组是“实”的地址,不能改变。当你和定义一个数组,则这个数组就得根据它在内存中的位置,得到一个地址,如上图中的“0×1A000000”。只要这个数组存在,那么它终生的地址就是这个值。

 

指针是一个“虚”的地址,可以改变地址的值。当你定义一个指针变量,这个变量占用4个字节的内存,你可以往这4字节的内存写入任意一个值,该值被当成一个内存地址。比如,你可以写入上面的“0×1A000000,此时,指针p指向第一个元素。也可以改为“0×1A000003”,此时,指针p指向第二个元素。

 

所以,当p通过 new [] 指向一段连续空间的结果是,p 是一个指向数组的指针,而*p是它所指的数组。

 

我们来看实例,首先看二者的类似之处。下面左边代码使用数组,右边代码使用指针。

 

数组 指针 (通过 new [] 所得)
//定义:

int arr[20];

//定义:

int* p = new int[20];

 

//让第一个元素值为100:

arr[0] = 100;

 

//让第一个元素值为100:

p[0] = 100;

//让后面19个元素值分别为其前一元素加 50

for (int i = 1; i < 20; i++)

{

   arr[i] = arr[i-1] + 50;

}

//让后面19个元素值分别为其前一元素加 50

for (int i = 1; i < 20; i++)

{

   p[i] = p[i-1] + 50;

}

//输出所有元素:

for (int i = 0; i < 20; i++)

{

  cout << arr[i] << endl;

}

//输出所有元素:

for (int i = 0; i < 20; i++)

{

  cout << p[i] << endl;

}

//也可以不用[],而通过+号来得到指定元素:

//当然,对于数组,更常用的还是 [] 操作符。

cout << *(arr + 0) << endl;  //*(arr+0) 等于 *arr

cout << *(arr + 1) << endl;

cout << *(arr + 1) << endl;

 

输出结果:

 100

 150

 200

//也可以不用[],而通过+号来得到指定元素:

//其实,对于指针,这样的+及-操作用得还要多点。

cout << *(p + 0) << endl; //*(p + 0) 等于 *p

cout << *(p + 1) << endl;

cout << *(p + 1) << endl;

 

输出结果:

 100

 150

 200

 

 

当指针变量 P 通过 new [] 指向一连续的内存空间:

1、p[N] 得到第N个元素 (0 <= N < 元素个数);2、*(p + N) 同样得到第N个元素 (0 <= N < 元素个数)

p[0] *(p + 0) 得到内存空间第0个元素;

 

把上面右边代码中的大部分 p 替换为 arr,则和左边代码变得一模一样。

 

下面再来比较二者的不同。

 

数组 指针
//定义并且初始化:

int arr[20] = {0,1,2,3,4,5,6,7,8,9,0,……,19};

 

//定义、并且生成空间,但不能直接初始空间的内容:

int* p = new int[20] {0,1,2,3,4 ……} //!!

 

//只得通过循环一个个设置:

for (int i = 0;  i < 20; i++)

{

   p[i] = i;

}

//不能通过对数组本身 + - 来改变数组的位置:

arr = arr + 1;  // !

cout << *arr << endl;

 

arr++;  // !

cout << *arr << endl;

 

arr–;  // !

cout << *arr << endl;

 

输出结果

无,因为程序有语法错误,过编译。

//可以通过 + - 操作直接改变指针:

p = p + 1;

cout << *p << endl;

 

p++;

cout << *p << endl;

 

p–;

cout << *p << endl;

 

输出结果:

 1

 2

 1

//释放空间:

//数组所带的空间由系统自动分配及回收

//无须也无法由程序来直接释放。

//释放空间:

//指向连续空间的指针,必须使用delete [] 来释放

delete [] p;

 

关于指针本身的 + 和 – 操作,请复习上一章相关内容。

 

接下来的问题也很重要。

20.1 理解指针的两种“改变”

普通变量(非指针,简单类型变量)只能改变值:

 

1) int a = 100;

2) …

3) a = 200;

 

1 行代码,声明int类型变量a,并且初始化a的值为100。

到第 3 行代码,变量a的值被改变成200。

 

对于非指针的简单变量,能被程序改变的,只有这一种。而指针变量,似乎有两种改变。

 

20.1.1 改变指针的值

这一点和普通变量一致。但要特别注意,“改变指针的值”引起的结果是什么?其实就是“改变指针的指向”。

因为,指针的值是某个变量的地址。假如指针P原来的值是A变量的地址,现在改为B变量的地址。我们称为:“P由指向A改为指向B”。这就是指针的第一种改变。

以下是示例代码:

int* P;

int A,B;

 

1) P = &A;

2) …

3) P = &B;

 

1) 行代码中,P 的值为 &A,即P指向变量A。

到3)行代码中,P的值变为&B,即改为指向变量B。

 

下面讲:指针的第二种改变通过指针,改变指针所指的变量的值。

 

20.1.2 改变指针所指的变量的值

 

复习前一章,我们知道通过 * (地址解析符),可以得到、或改变指针所指的变量的值。

 

int* P;

int A = 100;

 

P = &A;

*P = 200;

 

cout << A << endl;

 

代码中加粗的那一行:*P = 200; ,其作用完全等同于:A = 200;

所以,最后一行输出的结果是 200。

 

这就是指针的第二种改变:所指变量的值,被改变了。

 

20.1.3 两种改变?一种改变?

 

两种改变的意义不同:

改变一:改变指针本身的值(改变指向)。

改变二:改变指针指向的变量的值。

 

从代码上看:

第一种改变,P = &A; 左值(等号左边的值)是变量本身,右值则是一个地址。

而第二种改变,*P = 200; 左值通过星号对P操作,来取得P指向的变量;右值是普通的值。

 

理解,区分对指针的两种改变,才能学会如何使用指针。

 

请思考:上一章讲的“指针的加减操作”,是对指针的哪一种改变?

 

最后需要说明,严格意义上,指针仍然只有一种改变,即改变指针本身的值。改变指针指向的变量,应视为对另一变量的改变,只不过在代码上,它通过指针来进行,而不是直接对另一变量进行操作。

 

为指针分配、释放内存空间

 

之前,我们给指针下的定义是“指针是一个变量,它存放的值是另一个变量的地址”。

比如:

int a;

int* p = &a;

 

看,a 就是“另一个变量”,p指向了a。

我们知道,变量总是要占用一定的内存空间,比如上面的a,就占用了4个字节(sizeof(int))。这四个字节属于谁?当然属于变量a,而不是p。

 

现在要讲的是:也可以单独为指针分配一段新的内存空间。这一段内容不属于某个变量。

 

20.2 C++ 方式的内存分配与释放 new delete

在内存管理上,C++ 和 C 有着完全不同的两套方案。当然,C++的总是同时兼容C。C的那一套方案在C++里同样可行。

我们首先看看纯C++的那一套: new delete

new ,从字面上看意思为 “新”;而delete 字面意思为“删除”。二者在C++中内存管理中大致的功能,应是一个为“新建”,一个为“删除”。

 

20.2.1 new

 

new c++ 的一个关键字。被当作像 +、-、* 、/ 一样的操作符。它的操作结果是在申请到一段指定数据类型大小的内存。

 

语法:

 

指针变量 = new 数据类型;

 

new 将做三件事:

 

1、主动计算指定数据类型需要的内存空间大小;

2、返回正确的指针类型;

3、在分配内存的一,将按照语法规则,初始化所分配的内存。

 

这是什么意思呢?看看例子吧:

 

int* p;

p = new int;

 

和以往不一样,p 这回不再“寄人篱下”,并不是指向某个已存在的变量,而是直接指向一段由new 分配而来的新内存空间。

 

p 指向一段由new 分配而来的新内存空间” 这句话等同于:

new 分配一段新的内存空间,然后将该内存空间的地址存入到变量p中。”

所以,最终p中仍然是存储了一个变量的地址,只是,这是一个“无名”变量。

 

指向原有的某个变量,和指向一段新分配的内存空间,有什么区别呢?

“原有的变量”,可以比喻成指向一间原有的,并且有主的房间。而“新分配的内存空间”,则像是一个“临时建筑物”。我们必须在不用它的时候,主动将它拆迁。拆迁的工作由delete来完成。

 

当指针变量通过 new ,而得到一个内存地址后,我们就可以像以前的所说的,通过该指针,通过*号,而对该内存地址(一个无名的变量),进行操作。

如:

int* p = new int;

*p = 100;

cout << *p << endl;

 

屏幕将输出100。

 

20.2.2new 时初始化内存的值

 

new 也可以在申请内存空间时,直接设置该段内存里要放点什么.

 

语法:

 

指针变量 = new 数据类型(初值);

 

这样,上例可以改为:

 

int* p = new int(100);

cout << *p << endl;

 

如果你申请的是字符类型的空间,并且想初始化为‘A’:

 

char* pchar = new char(‘A’);

 

20.2.3 delete

 

语法:

delete 指针变量;

 

delete 将释放指定指针所指向的内存空间。

 

举例:

 

int* p;

p = new int;

 

*p = 100;

cout << *p << endl;

 

delete p;

 

system("PAUSE");

 

注意,当一个指针接受delete操作后,它就又成了一个“指向不明”的指针。尽管我们可以猜测它还是指向“原来的房子”,然而,事实上,那座“房子”已经被delete “拆迁”掉了。

 

20.2.4 实验: new delete

 

很简单的例子。

第一步:

首先,在CB新建一个控制台程序。然后把上一小节的代码放到main()函数内。运行。结果如下:

 

(new delete)

 

按任意键退出后,保存工程(Ctrl + Shift + S)

 

第二步:

接下来我们来观察指针变量被delete之后,所指向的内存会是什么。但,这是一件犯了C、C++编程大忌的事:访问一个已经delete 的指针的值。如果你最近运气很差,你的CB可能会被强行退出。所以,你明白我们为什么要先存盘了,对不?

 

在前面的代码中,加入以下加粗加红的一行(同时,你也应注意我的加的注释)

 

int* p;

p = new int;

 

*p = 100;

cout << *p << endl;

 

delete p;    //p所指向的内存空间已经被释放

 

cout << *p << endl;  //我们故意去访问此时p所指的内存

 

system("PAUSE");

 

运行结果:

 

(访问delete之后的指针)

 

44244844??在你的机器可能不是这个数,但一定同样是怪怪的值。 原来是好端端的100,现在却成了44244844。不要问我这是为什么?昨天来时,美眉还住在这里一座别致小阁楼里,今日故地重游,这里竟成废墟一片,依稀只见破墙上尚有:“拆!——城建局”的字样?!

 

new 是管建房的,而 delete就一个字:拆!

 

请大家自行在CB上完成本实验。我没有提供本题的实际工程。

2006年06月16日

19.8.7 * (地址解析符)与 ++ 的优先级

 

从上例中我们可以看到。当 * (作为地址解析符) ++ 同时作用在指针时,不管是前置还是++,都要比*有更高的优先级。比如代码中的:

 

int A = *parr++;

 

我们来一个反证:假设*的优先级比++高,那么,应先计算:

 

*parr 结果为: 1 (第一个元素)

然后计算  1++ ,结果为:2。

 

但实验发现结果为 1,这个1 又是如何来的呢?有点复杂。

首先,++优先计算,所以应先计算:parr++

结果是parr指向了下一个元素:2。因为这是后置++,所以,它必须返回自己计算之前的值;所以,在改变parr之前,编译程序会生成一个临时变量,计算原先parr的值。我们假设为 old_parr 。下面是第二步操作:

A = *old_parr

由于 old_parr parr 原来的值,指向第一个元素,所以 A 得到值: 1 。

 

可见,后置 ++ 或 后置操作,需要系统生成一个临时变量。

如果这个变量占用的内存空间很小(比如指针类型总是只有4字节),则该操作带来的,对程序速度的负面影响可以不计,如果变量很大,并且多次操作。则应在可能的情况下,尽量使用前置++或前置–操作。

 

你自然会问,前置++就不会产生临时变量吗?我们来试试。

 

int A = *++parr;

 

同样,++优先级大于*,所以先计算:++parr

结果parr 指向下一个元素。因为这是前置++,所以,它只需要返回的,正是自己计算之后的值。下一步是:

A = *parr;

由于 parr 此时已完成++操作,指向下一个元素。所以 A 得到值: 2

 

19.8.8 上机实验六:指针的 ++与–操作

 

int arr [] = {1,2,3,4,5};

 

int* parr = arr;

 

//前进 ++

for (int i=0; i < 5; i++) //如果为了优化,你可以写成: ++i :)

{

   cout << *parr << endl;

   parr++;   //如果为了优化,你可以写成:++parr :D

 

   /*

     上面两句你还可以写成一句: 

     cout << *parr++ << endl;  //这里,你可不能为了优化写成: *++parr.

  */

}

 

//后退 –:

for (int i=0; i < 5; i++)

{

   parr–;

   cout << *parr << endl;

}

 

输出结果:

(指针的++与–)

 

19.9 小结

 

指针是什么?不过也是一个变量,只是存储的是另一个变量的内容地址。

指针有多大?总是4字节。

 

如何定义指针? 多了一个*。

如何为指针赋值?全靠一个个&,除非对方已经是地址(如对方也是指针或是一个数组)。

如何得到指针所指的变量?如何通过指针改变所指变量?地址解析符:*。

如何移动指针?加加减减。

19.8 指针的加减操作

整型变量可以加减,求和,求差:

int a = 100;

int b = 99;

int c = a – b;

 

而指针,由于它存的是一个内存地址,那么我们可以想到,对两个指针进行求和,求差,是没有意义的。想想,把你家的门牌号(206)和对面的的门牌号(207)相加(413),得到的数有什么意义吗?

 

那么,指针的加减指什么呢?主要是指移动(而不是联通:()。比如,你家是206,那么,你的下一家,应该是206 + 1 = 207,而上一家则是206 – 1 = 205。你会说,我们这里的门牌号不是这样编的。但不管如何,大致应当是一个等差关系,或其它规律。都可以通过不断加上一个数,来得到下一家。

 

19.8.1 指向数组的指针

 

现在,来说说指针指向一个数组的情况。

 

int arr[] = {1,2,3,4,5}; //一个数组

 

int* parr; //一个指针。

 

parr = arr; //没有 & ?对啊,对数组就是不用取址符。

 

cout << *parr << endl;  //输出 *parr

 

先猜想一下,输出结果是什么?

 

最“直觉”的想法是:parr 指向一个数组,那么输出时,自然是输出数组中的所有元素了。所以答案应该是:“12345”了?

不过,我想,学过前面的数组,我们就能知道这种想法错误。

 

正确答案是输出数组中的第一个元素: 1 。

 

接下来,如果是这样输出呢?

 

parr = arr;

cout << parr << endl;

 

答案是输出了arr的地址。就等同于输出 arr

cout << arr << endl; 的作用

 

在这里,难点是要记住,数组变量本身就是地址。所以有:

 

1、想让指针变量存储一个数组的地址(想让指针变量指向一个数组)时,不用取址符。

2、解析一个指向数组的指针,得到的是数组的第一个元素。

 

我们来看示意图:

 

(指向数组的指针)

 

尽管数组中的每一个元素都有自已的地址,然而一个指向数组的指针,它仍然只是存储数组中第一个元素的地址。复习数组的章节,我们知道,数组中第一个元素的地址,就是数组的地址。即上图中的 10000010

 

事实上,如果我们想故意让代码很难理解,则对于这一句:

 

parr = arr;

可以改为:

parr = &arr[0];

 

本来嘛, arr 和 &arr[0] 的值就相等,我们在数组的章节已经学过。你若不信,可以输出一个:

cout << arr << "," << &arr[0] << endl;

 

19.8.2 上机实验三:指向数组的指针

 

int arr[2] = {1,2};

int* p = &arr[0];

 

//输出:指向数组的指针,数组的地址,数组中第一个元素的地址。

cout << p << "," << arr << "," << &arr[0] << endl;

 

system("PAUSE");

 

结果是:

(指向数组的指针,数组的地址,数组中第一个元素的地址。)

 

19.8.3 偏移指针

 

请看前图的这一部分:

 

parr 中存的值为 “10000010”。

 

指针可以进行加减操作。假设我现在再定义一个指针:

 

int* parr2;

 

parr2 = parr + 1;

 

现在问,parr2 的值是多少?有两种答案。一种说, parr 存的值是 10000010 ,加1后,自然应为 10000011了。这看似自然的答案又是错误了。

 

正确答案:10000014。继续看前图另一小部分:

(加1后,指针指向了下一个元素)

 

加1后,指针指向了下一个元素。由于这是一个 int 类型的数组,每个元素的大小是4个字节。所以第二个元素的地址是10000014。

 

重点 & 易错点:对指针 进行加1操作,得到的是下一个元素的地址,而不是原有地址值直接加1。

 

知到了如何“加”,也就知道了如何“减”。减以后,得到的是上一个元素的大小。

 

所以,一个类型为 T 的指针的移动,以 sizeof(T) 为移动单位。

 

比如:

int* pInt; 移动单位为 sizeof(int) 。即:4。而 char* pChar; 移动单位为 sizeof(char)。即1。

 

试举一例:

 

19.8.4 上机实验四:指针的最小移动单位

 

int arr[6] = {101,102,103,104,105,106};

int* pI = arr;

 

cout << "pI 是一个指向整型数组的指针,移动单位:4字节" << endl;

 

for (int i = 0; i < 6; i++)

   cout << "pI + " << i << " —-> " << pI + i << ", *(pI + i) = "  << *(pI + i) << endl;  

 

cout << "————————————" << endl;

 

//接下 来是一个指向char类型数组的指针:

char str[4] = {‘a’,'b’,'c’,'d’}

 

char*  pC = str;

 

cout << "pC 是一个指向字符数组的指针,移动单位:1字节" << endl;

for (int i=0; i < 4; i++)

    cout << "pC + " << i << " —-> " << (int)(pC + i) << ", *(pC + i) = "  << *(pC + i) << endl;  

 

system("PAUSE");

 

输出结果:

(指针的最小移动单位)

每一行中,程序先输出指针加上偏移量以后的值(地址),比如:1245024、1245028;然后输出偏移后指针指向的值,比如101,102。

查看移动前后指针存储的地址,我们就可以计算出移动单位。1245028 – 1245024 = 4 (byte)。

 

现在,我们回头再来看这道题:

 

有以下代码,设 arr 的地址是  10000010,请问最终 指针变量part2的值是多少?

 

int arr[] = {1,2,3,4};

int* parr1 = arr;

int* parr2;

int* parr2 = parr + 1;

 

答案: 10000010 + sizeof (int) = 10000014

 

这就是对指针加减操作的规则:假设某指针类型为 T* ,则该指针的最小移动单位为: sizeof(T)

即,若有:

T* p;

p + n = p + sizeof(T) * n; 及: p – n = p – sizeof(T) * n;

 

19.8.5 指针的 += -= 、++、 — 操作

 

C、C++ 除了“传统”的 +,- 操作以外,还提供了如题的四种加减操作。这些对于指针同样适用。

 

int arr[] = {1,2,3,4,5,6,7,8,9,10};

int* parr1 = arr;

 

parr1 += 1; //向后称动一个单位

 

parr1 += 1; 的结果,相当于:  parr1 = parr1 + 2;

我们再前面的例子是: parr2 = parr1 + 2; 所以计算结果赋值给parr2,所以 parr2 指向了parr1 所指的下一个元素的位置。parr1本身仍然指在第一个元素。

但对于 parr1 += 1 parr1 = parr1 + 2; 则改变的是 parr1自身的值。

 

现在,如果来加一句:

 

parr1 -= 1; //向前移动一个单位

parr1 又回到 arr 数组的开始位置。

 

也可以直接移动2个或更多个单位:

 

parr1 = arr;

parr1 += 2;  //parr1 现在指向:元素 3

parr1 -= 2;  //parr1 现在又回到了 元素 1 上面

 

++ 和 — 操作 运算结果等同于 += 1 -= 1

 

//后置:

parr1++;

parr1–;

 

//前置:

++parr1;

–parr1;

 

前置与后置的区别,请复习我们第一次讲 ++ 和 –的章节内容。这时仅举一例,用两段代码来对比,请大家思考,并且最好把它写成实际程序运行。

 

19.8.6 上机实验五:指针的前置++与后置++的区别

 

//代码片段一:

int arr[] = {1,2,3,4,5,6,7,8,9,10};

int* parr1 = arr;

 

int A = *parr1++;

int B = *parr1;

 

cout << "A = " << A << endl;

cout << "B = " << B << endl;

 

输出结果:

 

A = 1;

B = 2;

 

代码片段二:

 

int arr[] = {1,2,3,4,5,6,7,8,9,10};

int* parr1 = arr;

 

int A = *++parr1;

int B = *parr1;

 

cout << "A = " << A << endl;

cout << "B = " << B << endl;

 

输出结果:

 

A = 2;

B = 2;

 

19.4.2 给指针变量赋值

 

(1)        int k = 100;

(2)        int* p;

(3)        p = &k;   //p取得变量k的地址 (也称:p指向k)

 

第1行定义一个整型变量k。

第2行定义了一个整型指针p。

而第3行,“指针 p 存储了变量k的地址”。短的说法是“p指向了k”。

 

执行了上面三行代码后,结果是: p 指向 k。我们来看具体的内存示意图。

 

p 指向 k 示意图

 

上图中,红色数字代表什么?红数字11000002 是 变量 k 的内存地址。

 

指针初始化的结果,就是让它存储下另一个变量的地址。简称指向另一个变量。

 

下面说说三种常见的,给指针赋值的操作:

 

一、用 & 取得普通变量的地址。

 

要想让指针指向某个普通变量,需要通过 & 来得到该普通变量的地址。

 

int k;

int* p = &k;

 

 

二、指针之间的赋值。

两个指针之间也可以相互赋值,此时不需要通过 & 来取址。

 

int k;

 

int* p1

int* p2;

 

p1 = &k;  //p1 先指向 k

p2 = p1;  //然后 ,p2 也指向 k。

 

注意,p2 = p1的结果是:让p2也指向“p1所指的内容”,而不是让p2指向“p1本身”。

上面的情况可以改为直接在定义时就赋值:

int k;

 

int* p1 = &k;

int* p2 = p1;

 

三、让指针指向数组

数组本身就是地址,所以对数组变量也不用通过 & 来取址。

 

char name[] = "NanYu";

char* p = name;  //不用取址符 &

 

或者:

 

int arr[3] = {100,99,80};

int* p = arr; 

 

四、让指针指向一个新地址

 

前而的赋值方法都是让指针指向一个已有的内存空间的地址。比如:int* p = &k; 指针变量p 存放的是已有的变量k的地址。其实指针也可以指向一个新开辟的内存空间,这一内存空间不归属于其它变量。

 

在C++中,常用一个关键字:new 来为指针开辟一段空间。比如:

 

int* p = new int;

 

现在,指针变量p存储着一个内存地址,该内存地址确实存在——它是由 new 操作符申请而得。可以这样认为,new 是一位特权人物,不通过它,指针只能指向已有的“房间”;而使用了它,则可以要求系统为指针“新开辟一片空地,然后建上新房间”。

有特权的人不仅仅是“new”,还有几个系统定义的函数,及Windows提供的函数,都可以实现“向系统要空间”的功能。我们将在后面专门的 章节详细讲解。

 

19.5 地址解析 及其操作符 *

 

* 在C,C++语言里除了起“乘号”的作用以外,前面我们还刚学了它可以在定义变量时,表明某个变量是属于“指针类型”。现在,则要讲到它还能起“地址解析”的作用。

 

什么叫“地址解析”?假设有一 int 类型变量k:

 

int k = 100;

 


内存 内存地址
100 11000000

 

方框是变量k所占用的内存。100 是该内存中存放的。而 11000000 则是该内存的地址。

 

“地址解析”就是 地址-> 的解释过程。即:通过地址 11000000 得到位于地址的变量。

可见“地址解析(*)” 和 “取址(&)” 正好是一对相反的操作。

 

这好有一比:地址解析符 * 是个邮递员,他要根据信封上的地址,找到实际的房子。而 取址符 & 则是当初到你家抄下门牌号的人。

 

看一下实际应用例子:

 

int k = 100;

int* p = &k;

int m = *p;

 

cout << m << endl;

 

执行以上代码,屏幕上将输出 100。

实际上也可以这样写,以取得相同结果:

 

int k = 100;

int* p = &k;

 

cout << *p << endl;

 

直接输出 *p 即可。*p 的值就是 100.

 

明白了吗? p = &k 让p得到k的地址,而 *p 则得到k的值。

下面就考一考你 & * 的作用。

 

int k = 100;

cout << *&k << endl;

 

将会输出什么?

 

通过地址解析得到变量,不仅仅可以“得知”该变量的值,还可直接修改该变量的值。

 

int k = 100;

int* p = &k; //p 指向k

 

*p = -100;

cout << k << endl;

 

屏幕上将输出 -100。

 

19.6 上机实验一 指向普通变量的指针和指针之间的互相赋值

 

实验一有多个步骤。

 

第一步、将上面的代码片段变成程序。请大家打开CB,新建空白控制台工程,然后输入以下代码。

 

1) int k = 100;

2) int* p1 = &k;

 

3) cout << *p1 << endl;

 

4) system("PAUSE");

 

(行前的编号是为下面讲课方便,你当然不能将它们输入到CB中去。)

 

运行将查看结果。然后按任意键退出该程序。下面我们要亲眼看看,指针变量p1,是否真的存放着普通变量k的地址。

 

把断点设在第2行上。(输入光标移到第二行,然后按F5)

(设置断点)

 

按F9运行,程序停在断点上,然后按 Ctrl + F7 调出"Evaluate/Modify" 对话框。该对话框可以在程序运行时观察或强行修改某个变量的值。我们这里仅是想看看变量 k 的地址。所以如图输入 &k ,回车后,Result(结果)栏出现:0012FF88。这就是k的地址。

 

(&k 的值就是 k 的内存地址)

 

接下来我们该看 p1 的值。看它是否等于 0012FF88。删除 &k ,改为 p1,回车,怎么回事?CB竟然很不配合地显示p1的值为 00000100 ?呵,这是因为当前断点那一行还没有运行呢,p1现在的值是随机的,这验证好我们前面说的“指向不明”。

(还没有初始化的指针p1,随随便便指向一个莫名的地址)

 

关掉该对话框,然后按F8再运行一步。再按 Ctrl + F7 调出上面窗口,输入 p1,回车,这回显示的的值正合我们的推想。

(p1 的值,就是 k 的地址,即:p1 等于 &k

 

 

第二步、基于前一题,再加上一个指针变量,叫 p2

 

1) int k = 100;

2) int* p1 = &k;

3) int* p2 = p;

 

4) cout << *p1 << endl;

5) cout << *p2 << endl;

 

编译并运行,观察结果应发现,*p *p2 值相等,为什么?因为二者指向同一变量:k。

 

第三步、请像第一步一样,观察,k 的地址,及p1, p2 的值。看三者是否相等。

 

最后,将后面的两行输出删除,改为以下两行代码。第一行输出 k 的地址、p1、p2的值。

第二行 输出k的值、*p1、*p2 值。

 

//cout << *p1 << endl;

//cout << *p2 << endl;

 

cout << "&k = " << &k << ", p1 = " << p1 << ", p2 = " << p2 <<endl;

cout << "k = " << k << ", *p1 = " << *p1 << ", *p2 = " << *p2 <<endl;
 

 

运行后结果如下:

(1245064 是十进制的,它等于十六进制的 0×0012FF88)

 

19.7 上机实验二:改变指针所指变量的值,改变指针的指向

 

尽管在上面的例子中修修改改也能改成本例,不过枝节太多的代码不会混淆了我们的目的。这次我们重点在于“改变”。

 

第一步、 通过指针,改变其所指向的变量的值。(好绕啊!代码却很简单)

 

int k = 100;

 

int* p = &k;

 

//先输出一开始的k和*p的值(用逗号分开)

cout << k << "," << *p << endl; 

 

//现在直接改变k值:

k = 200;

 

//输出此时的二者的值:

cout << k << "," << *p << endl; 

 

//然后通过指针来改变k值:

*p = 300;

 

//输出此时的二者的值:

cout << k << "," << *p << endl; 

 

system("PAUSE");

 

输出将是:

 

 

可见,当p指向k以后,修改 *p 的值完全等同于直接修改 k值。

 

第二步、改变指针的指向

 

所谓的“改变指向”,其实就是“改变指针变量中存储的值(另一个变量的地址)”。我们一开始说的,两种不同的说法而已。

在前面的代码最后,我们加上以下代码:

 

int m = 1000;

 

//现在p改为指向变量 m :

p = &m;

 

 k = 400;

cout << k << "," << m << "," << *p << endl;
 

*p = 2000;

cout << k << "," << m << "," << *p << endl;

 

system("PAUSE");

 

屏幕输出是:

 

当p改为指向m以后,之前指向的k便再也和它没有什么关系了。改变k值不会再影响p;而改变p值,则影响到m值而非k值。

19.1 指针是什么?

 

当我们说“人”这个词时,有时指的是“人类”如:“人是一种会笑的动物”,有时则指个体:比如“张三这人”。

 

“指针”这个要念也一样,它可以意指“指针类型”,也可以代表某个具体的“指针变量”。下面我们重点要讲的是:“什么叫指针变量”。

 

所以这一小节的题目其实应是:“指针变量是什么?”

 

“指针变量”是什么?诚如其名,它首先是一个变量。

 

变量最重要的特性是什么?那就是它可以存储一个值。比如:

 

下面是三行代码,行号是我为了表达方便而特意加上的,并不在实际代码中出现。

 

(1)   int a;

(2)   a = 100;

(3)   a = 200;

 

第(1)行定义了一个变量:a。现在它存储的那个值的大小是不定的,因为我们还没有赋给它具体的值呢。

到了(2)行执行以后,a 就存了一个值,这个值多大?答:100。

这里举的是一个整型变量,但其实无论是什么类型的变量,都是用来存值的。

并且,变量的值可以在以后改变大小,比如第(3)行,a中存放的值成了200。

 

回忆完普通变量的特性,现在说“指针变量”。

 

指针也是变量,所以也是用于存储一个值。重点是,它存储的这个值,意义有点特别。

 

指针存储的不是普通的一个值,而是另外一个变量的地址。

 

一句话:指针是一种用于储存“另外一个变量的地址”的变量。或者拆成两句:指针是一个变量,它的值是另外一个变量的地址。

 

这就是指针变量与其它的变量的同与不同:的都是一个变量,都用来存储一个值;但,指针存放的是另外一个变量的地址

 

可以这样打个比方:

 

有一间房子,它的地址是:人民路108号。这个房子相当于一个变量。那么:

一、如果它是普通变量,则房子里可能今天住的是张三,明天住的是李四。张三,李四就是这个变量的值。通过访问这间房子,我们可以直接找到张三或李四。

 

二、如果它是一个指针变量,则房子里不住具体的人,而是放一张纸条,上面写:“南京东路77号”。

“南京东路77号”是一个什么东西?是一个地址。

通过该地址,我们继续找,结果在“南京东路77号”里找到张三。

变量的存储的值可以改变,指针变量的值同样可以变更:

过一天,我们再去访问这个房子,纸条变了“珠海路309号”,通过它,我们找到的是另一个人。

 

图解:

 

能够说出图中两个“20000007”的关系吗?

 

本质就这点不同,同样是变量,同样要占用一定大小的内存空间,不同的是普通变量在那个内存里,存储了一个具体的值;而指针变量存放的是另一个变量的地址。

 

不过,什么叫“变量的地址”?只要你是一直跟着我们的课程学习到这里,相信你能明白什么叫变量的“地址”。否则,您可以需要从第一章从头看起了。

 

说到这里,我们其实一直在说的是“指针变量”,而不是“指针类型”。指针也需要类型,它所存储的那个变量类型,就称为指针的类型。继续前面的比方,有一天我们去人民路108号,今天纸条写着的地址是:“美眉街8号”,于是我们兴冲冲地去了……结果“美眉街8号”里住着一头猪!是不是很失落——我们以为是“人类”,并且是“美眉”,未料却关着一头“猪类”?!

 

计算机虽然不懂得什么叫“失落”,但为了它的准确性,我们需要事先定义好一个指针到底是放的是什么类型的变量。这个类型通常也当作是该指针的类型。

 

“指针变量中存储某变量的地址”这种说法是不是有些绕?所以有一个简短的说法:“指针指向某一变量”。

 这种说法的缺陷是不能像第一种说法好样道出指针的本质。但它确实方便。下面我们将不断的使用这两种说法,大家需要知道它们具有相同意义。

 

19.2 指针的大小

 

指针的大小是问:一个指针变量占用多少内存空间?

 

分析:既然指针只是要存储另一个变量的地址,。注意,是存放一变量的地址,而不是存放一个变量本身,所以,不管指针指向什么类型的变量,它的大小总是固定的:只要能放得下一个地址就行!(这是一间只有烟盒大小的“房间”,因为它只需要入一张与着地址的纸条)。

 

存放一个地址需要几个字节?答案是和一个 int 类型的大小相同:4字节。

 

所以,若有:

int* pInt;

char* pChar;

bool* pBool;

float* pFloat;

double* pDouble;

 

: sizeof(pInt)、sizeof(pChar)、sizeof(pBool)、sizeof(pFloat)、sizeof(pDouble)的值全部为:4。

 

(你敢拆电脑吗?拆开电脑,认得硬盘数据线吗?仔细数数那扁宽的数据线由几条细线组成?答案:32条,正是 4 * 8)。

 

我们这一章有很多上机实验。这就算是第一个,只是我提供了代码:请写一个程序,验证上面关于sizeof(T *)的结论。在写程序之前,务必要先参考一下“数据结构”这一章中sizeof的例子。

 

19.3 定义一个指针变量

 

数据类型*  变量名;

或:

数据类型  *变量名;

 

和申请一个普通变量相比,只是在数据类型后面多了一个星号。比如:

 

int* p;

 

星号也可以靠在变量名前面,如:

 

int *p;

 

要同时定义多个相同类型的指针,则为:

 

int *p1, *p2;

 

注意,每个变量之前都必须有 * 。

 

19.4 初始化指针变量

是变量就存在一个初始化的问题。一个不能确定是住着什么人的房间,总是有点恐怖。

 

19.4.1 指向不明的指针

我先定义一个整型指针:

int* p;

 

现在,p是一个指针,int 规定它只能存放整型变量的地址,而不是其它如字符型,布尔型等等。

我们称:p 是一个整型指针。

 

不过,现在 p 指向哪里 (即:p 存储的是哪个变量的地址 )?

变量在没有赋值之前,其值不定的。对于指针变量,值不定可以表述为:指向不明。

 

重点来了! 一个指向不明的指针,是一个危险的家伙。很多软件有BUG,其最后的原因,就在这里。

来看看下而的“恐怖片”:

 

你来到一间阴森森的房间,这房间里有一张纸条 > 打开火折,但见

纸条内容:“XXX街3K号

> 你前往这纸条的神秘地址…… > “XXX街3K号”里住着一千年老妖!你……
程序访问了一个没有初始化的指针:

int* p;

p 的内存是随机的一个数,比如: 0×3FF0073D 程序随即访问内存地址:

 0×3FF0073D

0×3FF0073D 是哪里的内存?说不定正好是Windows老大要用的内存,你竟敢访问!

Windows一生气,蓝屏。

                     

 

既然没有赋值的指针这么危险,下面来看看如何给指针赋值。

2006年06月14日

自己总结的,觉得能帮助初学者看透一些纷繁复杂的语法规则,理解C语言的真谛 
第一次发布,不一定正确,欢迎讨论、指正、补充 

1. 表达式定律 

   任何能产生数值结果的运算、操作都可以作为表达式,并可以放到任何需要数值结果的地方,只要数值类型能够匹配 

   常见的可以产生数值结果的运算和操作 

   算术、逻辑、位运算等 
   ? : 
   &、*等操作 
   有返回值的函数 

   常见的需要数值的地方有: 

   赋值 
   条件判断 
   函数调用 

2. 类型定律 

   任何类型都可以在任何需要类型的地方使用 

   已知特例 

   函数返回值不能定义为数组类型 
   常用类型 

   基本数据类型、指针、数组、结构…… 
   常见的需要类型的地方 

   定义变量 
   定义指针、数组和结构 
   函数参数和返回值 
   sizeof 

3. 参数传递定律 

   函数调用时的参数传递永远都是传值调用,把实参的值拷贝给形参 

   实参:调用者提供的参数 
   形参:函数定义的参数 
   基本数据类型无容置疑 
   struct也无容置疑 
   指针作为参数时,把指针变量的内容(就是其指向的内存地址)做了拷贝 
   数组名作为参数时,把它等同于指针看待了。