存档在 2011年2月

基于Qt的Http编程-进阶

2011年2月27日

进阶

上文的Http客户端只能下载指定网址的数据,这样的客户端在交互性和功能性上都很差。本文所描述的程序则在这个基本的客户端上进行改造,实现任意目标地址的数据下载,并且改善了用户界面的。具体UI可参考下图:

对于UI而言,该客户端增加了任意地址的输入框、下载进度条和下载按钮;对于下载的数据而言,该客户端不再局限于下载文本数据。接下来本文将按照程序执行的大致顺序对相关函数进行分析。

构造函数

用户界面的设计可见本文参考1和参考2,这里从构造函数开始。在构造函数中除了对用户界面进行初始化外,还创建了一个QNetworkAccessManager对象,并将进度显示条隐藏了起来。

widget::widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::widget)
{
    ui->setupUi(this);
    manager = new QNetworkAccessManager(this);
    ui->progressBar->hide();
}

on_pushButton_clicked槽函数

该槽函数对应着下载按钮的单击事件。该函数完成的工作有从用户界面获得下载地址;从这个地址中解析出所下载文件的名称;打开(创建)所下载的文件;发送下载请求;更新进度显示条的信息。

从上文中可以看到,在Qt中使用QUrl类来存储url地址,通过该类的成员函数还可以根据具体情况对url地址进行相应的处理。使用QFileInfo类的对象存储不依赖具体系统的文件属性,比如文件的名称,路径,访问权限等;通过该类的成员函数可以方便的获取文件的某些属性,比如通过fileName成员函数就可以从路径名中快速解析出文件名。

得到了文件名就可以创建一个QFile类的对象,通过open成员函数就可以打开这个文件,QIODevice::WriteOnly为打开模式。如果打开错误,则弹出警告提示框,并进行相应的错误处理。

文件打开成功后,紧接着就应该发送下载链接的请求了,这个工作将在startRequest()中完成。最后设置进度更新条的初始值,并将其在界面上显示出来。具体的实现代码可参考下图:

void widget::on_pushButton_clicked()
{
    url.setUrl(ui->lineEdit->text());
    QFileInfo info(url.path());//获得地址;
    QString fileName(info.fileName());//从地址中获得文件名;

    //如果地址类似www.edsionte.com,则文件名为index.html;
    if (fileName.isEmpty())
        fileName = "index.html";

    file = new QFile(fileName);
    if (!file->open(QIODevice::WriteOnly))
    {
        QMessageBox::warning(this, tr("Warning"),
                             tr("file open error"),
                             QMessageBox::Yes);
        qDebug() << "file open error";
        delete file;
        file = 0;
        return;
   }
   startRequest(url);//进行请求;
   ui->progressBar->setValue(0);
   ui->progressBar->show();
}

startRequest()的实现

通过参考下面的示例代码就可以发现,该函数注意进行了两部分的内容:发送下载请求并获得数据回复和几组信号和槽函数之间的连接。

get()函数在上文中已有说明,它将返回一个QNetworkReply类的对象reply。当所有的数据下载完毕后,manager对象将发送finished信号,进而调用httpFinished槽函数。

上文所描述的Http客户端是等待所有的数据都下载到内存再读出并显示,而本文所描述的Http客户端则将请求的数据进行分段下载并保存。因此每当有一部分新数据到达本地,reply就会发送readyRead信号,进而调用httpReadyRead槽函数将这部分新的数据保存到本地。使用这种分段下载并保存数据的方法可以有效的节省内存。而一旦网络请求有数据返回,reply对象就会发送downloadProgress信号,进而引发updataReadProcess槽函数对进度显示条进行更新。

void widget::startRequest(QUrl url)
{
    reply = manager->get(QNetworkRequest(url));

    connect(reply, SIGNAL(finished()),
            this, SLOT(httpFinished()));

    connect(reply, SIGNAL(readyRead()),
            this, SLOT(httpReadyRead()));

    connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
            this, SLOT(updataReadProcess(qint64,qint64)));
}

相关槽函数

当有一部分新数据返回到本地后,就回执行httpReadyRead函数。该函数将这部分新到达内存的数据写入file对象所对应的文件中。

void widget::httpReadyRead()
{
    //如果文件存在,则将数据写入文件;
    if (file)
        file->write(reply->readAll());
}

每当请求的数据有返回时,就执行updataReadProcess函数以便及时更新进度显示条。该函数包含两个参数byteRead和totalBytes。前者表示当前接受到的数据总量,后者表示应该下载的数据总量。显然,随着byteRead的增加,进度显示条也不断更新。当byteRead和totalBytes相等时,表示下载完毕。

void widget::updataReadProcess(qint64 byteRead, qint64 totalBytes)
{
    ui->progressBar->setMaximum(totalBytes);//最大值;
    ui->progressBar->setValue(byteRead);//当前值
}

当所有数据都下载完毕后,就执行httpFinished函数。在该函数中将隐藏进度显示条,并将缓冲区的数据清空。并且关闭文件对象,再释放之前申请的一些数据空间。

void widget::httpFinished()
{
    ui->progressBar->hide();
    file->flush();
    file->close();
    reply->deleteLater();
    reply = 0;
    delete file;
    file = 0;
}

至此,基本上完成了一个Http下载客户端。理论上可以下载任何数据,不过我在测试中发现有些地址不能如期下载(比如QQ的下载链接)。

参考:

1. Qt Assistant

2. http://www.yafeilinux.com/?p=734

3. C++ GUI Qt 4编程(第二版); 电子工业出版社;Blanchette,J,Summerfield,M 著;闫锋欣 等译;

基于Qt的Http编程-基本原理

2011年2月26日

在Qt中,使用QNetworkAccessManager类就可以完成基于Http协议的数据上传和下载,该类既可以发送网络请求,也可以接收网络回复。而具体的网络请求是通过QNetworkRequest类发送的,具体的网络回复是通过QNetworkReply类来接收的。

本文将利用上面的几个类实现一个简单的Http客户端,从指定的网址下载数据。

基本原理

由于QNetworkAccessManager类中包含了一组标准的数据请求函数,因此可以通过该类的对象发送数据请求函数;每个请求函数执行完毕时都回返回一个QNetworkReply对象。当所有请求的数据都到达本地后,将引发一个finished()信号,该信号关联了一个处理返回数据的槽函数。具体的实现可参考下述代码:

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    manager = new QNetworkAccessManager(this);
    connect(manager, SIGNAL(finished(QNetworkReply *)),
            this, SLOT(replyFinished(QNetworkReply*)));

    manager->get(QNetworkRequest(QUrl(
            "http://www.kerneltravel.net/")));
}

可以看到,上述的基本原理大部分都在构造函数中完成。首先创建了一个QNetworkAccessManager对象manager;接着将manager所引发的finished()信号与replyFinished()槽进行关联;最后通过get()发送数据请求。

get()用于发送请求并获得目标地址中的数据,具体的数据请求则是通过创建一个QNetworkRequest类的对象而完成的。只要数据请求发送成功,则开始下载数据。当所有的数据下载完成后,就返回一个QNetworkReply类型的对象。同时manager对象将发送一个finished()信号,引发replyFinished槽函数的执行。

当执行上述的槽函数时,就说明目标地址的数据已经下载完毕。此时槽函数要做的就是将这些数据显示出来。这里我们只只对文本数据进行转换。对这些数据的转换动作可参考下述的代码:

void Widget::replyFinished(QNetworkReply *reply)
{
    QTextCodec *codec = QTextCodec::codecForName("utf8");

    QString all = codec->toUnicode(reply->readAll());
    ui->textEdit->setText(all);
    reply->deleteLater();
}

为了能够正确显示中文,我们创建QTextCodec对象。利用readAll函数可以读取数据请求返回的所有数据,并且利用toUnicode函数将这些数据转换成QString类型。最后在用户界面中的文本编辑器中显示出来。

按照上面的方法就可以下载指定地址的数据。如下图:

当返回的数据显示完毕后,利用deleteLater函数将返回的数据删除。

上述的执行过程用数据流图表示如下:

上述内容只是对基于Http协议的数据下载做了简单的描述,并没有对界面设计作过多的介绍,关于界面的设计可见参考1。下文将对这个基本的Http客户端进行改造。

参考:

1. Qt Assistant

2. http://www.yafeilinux.com/?p=734

3. C++ GUI Qt 4编程(第二版); 电子工业出版社;Blanchette,J,Summerfield,M 著;闫锋欣 等译;

基于Qt4的GUI开发流程

2011年2月19日

最近在学习使用Qt4进行GUI的开发。经过几天的学习,基本上清楚了大致的开发流程。本文以一个简单的文本编辑器为例,说明Qt4的基本开发流程。

之所以选用文本编辑器为例是因为它属于一个标准的主窗口,整个用户界面包含菜单、工具栏、状态栏以及应用程序所需要的对话框。效果图如下:

类似这样的主窗口实现需要三个步骤:用户界面的设计、建立信号与槽之间的连接和自定义槽的实现。Qt4为开发人员提供了两种实现用户界面的方法,一种是通过传统的编码方式实现用户界面;另一种是通过Qt designer(现被集成到Qt Creator中)快速实现。这两种实现方式只是体现在用户界面的设计上,虽然在后续的两个步骤中稍有不同,但本质上是相同的。为了方便说明,本文将这两种设计方式分开描述。

1. 使用编码方式实现

此部分所描述的文本编辑器对应的工程文件为code_myTextEdit,该文本编辑器对应的类为MainWindow,它继承了QMainWindow类。该工程内部所有的文件构成如下图:

1.1 用户界面的实现

如果使用编码方式,则需要在定义MainWindow类的时候声明用户界面所需要的所有部件。比如该文本编辑器的有三个菜单,则需要分别声明三个QMenu类型的变量:menu_File、menu_Edit和menu_Help。此外,还需要声明一些QAction类型的变量,该类型的变量代表鼠标点击菜单或工具栏上相应选项时所产生的动作。比如File菜单中的New选项就对应QAction类型的变量newAct。除上述的成员变量外,在类的定义中还必须声明所需的槽函数。以上这些声明通常被单独放在一个头文件中,即mainwindow.h。

声明完毕后,对用户界面的建立常常会被安排在对应类的构造函数中。其实现通常在.cpp文件中完成,比如mainwindow.cpp。比如在MainWindow类的构造函数中就有createMenus()、createActions()、createToolBars()和createStatusBar()几个函数,他们分别创建主窗口的菜单、菜单选项、工具栏和状态栏。

1.2 建立信号和槽之间的连接

第一部分的代码实现了用户界面,接下来应该建立信号和槽之间的连接。Qt中的信号和槽机制与Java中的事件驱动机制类似,这里的信号相当于用户所引发的事件,而槽本质上就是一个函数,它实现了这个动作所对应的功能。

Qt中使用connect函数来建立信号和槽之间的链接。通常一个信号可以链接多个槽,多个信号也可以链接同一个槽,并且一个信号还可以与另一个信号相连接。连接函数通常放在相关类的构造函数中完成,上述的文本编辑器所对应的MainWindow类的构造函数中,createActions()不仅创建了相应事件的动作对象,而且完成了所有可能引发的事件与相应槽之间的连接。下面是New菜单选项的创建代码:

    newAct = new QAction(tr("&New"), this);
    newAct->setShortcuts(QKeySequence::New);
    newAct->setStatusTip(tr("Create a new file"));
    connect(newAct, SIGNAL(triggered()), this, SLOT(newFile()));

connect函数说明当用户对New选项发出“触发”动作时,当前的对象就调用newFile函数进行事件处理。这里的触发即包含鼠标点击又包含快捷键等。

1.3 实现自定义的槽函数

这部分应该是最关键的一步,根据每个事件的具体功能实现相应的槽函数。比如下面的代码就是实现New菜单选项:

void MainWindow::newFile()
{
    if (maybeSave()) {
        textEdit->clear();
        setCurrentFile("");
    }
}

其中maybeSave()用来判断当前已修改的文件是否进行了保存;如果已保存则清空当前的文本编辑器,并设置相应的主窗口的标题名。其中,maybeSave()和setCurrentFile()都需要用户自己实现。

将第一步声明的所有槽函数都实现后,文本编辑器也基本上算开发完成。

2. 使用设计方式实现

2.1 用户界面的实现

相比传统的编码方式,使用Qt designer则可以快速设计出所需要的用户界面。通过从工具栏中拖动相应的部件到主窗口就可以完成用户界面的涉及。关于主窗口的设计可以参考Qt在线手册中的Creating Main Windows in Qt Designer一节,这里不再赘述。此部分所说的文本编辑器对应的工程文件为design_myTextEdit,内部文件如下图:

如果使用这种可视化的设计方式,则会使用.ui文件保存这个用户界面。用户界面编译器(user interface compiler,uic)则会将.ui文件转换成适合C++的.h文件。比如上述的文本编辑器的用户界面存储在mainwindow.ui中,uic会将其转换成ui_mainwindow.h文件。所生成的ui_mainwindow.h文件中包含了类Ui::MainWindow的定义,该类是一个与mainwindow.ui等价的C++文件。这个类中声明了用户界面中的一些部件以及初始化窗体的setupUi()。

具体在使用时,则通常再定义一个新的类MainWindow,让这个类继承QMainWindow和Ui::MainWindow两个类。此时,就不需要MainWindow中声明窗体中的各个部件了,因为这已经在mainwindow.ui文件中完成;同时对窗体的初始化也可以通过调用setupUi(this)自动完成。这一点是与用编码方式实现用户界面所不同的。

2.2 建立信号和槽之间的连接

使用设计方式创建完主窗口后,建立信号和槽之间的连接也十分简单。只需在事件编辑器中选取具体事件和具体的触发动作,并点击右键的go to slot选项即可在mainwindow.h和mainwindow.cpp文件中创建相应槽函数声明。

2.3 实现自定义的槽函数

到此步只需在实现相应的槽函数即可。比如上述save选项对应的槽如下所示,现在只需输入相应的实现代码即可。

void MainWindow::on_action_Save_triggered()
{
    save();
}

因此,从上述内容来看,创建用户界面的这两种方式在本质上是相同的。虽然设计方式简单方便,但是对于初学者最好两种方法都掌握,这样才能理解用户界面的开发方式。

参考:

1. Qt Assistant

2. http://www.yafeilinux.com/?page_id=3

3. C++ GUI Qt 4编程(第二版); 电子工业出版社;Blanchette,J,Summerfield,M 著;闫锋欣 等译;

 

多维数组那回事儿

2011年2月10日

前面几篇“那回事儿”的文章更强调一维组和指针之间的关系,本文关注的是多维数组,即“数组的数组”。

多维数组

我们可以将多维数组抽象的看作是具有某种类型的一维数组。当“某种类型”为基本的数据类型时,多维数组就退化成普通的一维数组;当“某种类型”仍然为数组时,那么就形成了多维数组。也就是说任何一个多维数组都可以分解成几个一维数组。

下面通过示例程序来深入了解多维数组ma[2][3]的构成。

#include < stdio.h >

int main()
{
	int ma[2][3];
	int (*r)[2][3];
	int (*p)[3];
	int *t;

	/*代码段1*/
	p = ma;
	printf("sizeof(ma[0])=%d\n",sizeof(ma[0]));
	printf("ma      =%p\tp   =%p\n",ma,p);
	printf("p+1 =%p\n",p+1);
	/*代码段2*/
	r = &ma;
	printf("sizeof(ma)=%d\n",sizeof(ma));
	printf("&ma     =%p\tr  =%p\n",&ma,r);
	printf("&ma+1   =%p\tr+1=%p\n",&ma+1,r+1);
	/*代码段3*/
	t = ma[0];
	printf("sizeof(ma[0][0])=%d\n",sizeof(ma[0][0]));
	printf("ma[0]   =%p\tt   =%p\n",ma[0],t);
	printf("ma[0]+1 =%p\tt+1 =%p\n",ma[0]+1,t+1);
	return 0;
}

由多维数组ma最左维的长度2可知,ma数组包含两个元素ma[0]和ma[1]。数组名ma在表达式中是数组ma首元素的首地址。在代码段1中将ma赋值给数组指针p,则p指向多维数组ma的首元素ma[0],则p+1指向第二个元素ma[1]。其中p是一个数组指针,它指向一个长度为3的数组,则指针p每次移动的偏移量为12。可参考下图:


在代码2中对ma取地址并将其赋值给指针r。r现在指向一个“第一维的大小为2,第二维的大小为3的数组”,则r+1将指向下一个这样的数组(尽管这样的数组并不存在)。由此也可得知r每次的偏移量为24。


ma[0]和ma[1]都是一个长度为3的整型数组,现在以ma[0]为例进行说明。ma[0]中包含三个元素ma[0][0],ma[0][1]和ma[0][2]。在代码段3中将ma[0]赋值给t,则t指向数组ma[0]的第一个元素a[0][0],则t+1和t+2则依次指向第二个元素和第三个元素。


对多维数组ma的结构有了一定了解后,现在再看上述程序的运行结果:

edsionte@edsionte-laptop:~/code/expertC$ gcc array.c -o array
edsionte@edsionte-laptop:~/code/expertC$ ./array
sizeof(ma[0])=12
ma   =0xbfdfaa6c	p=0xbfdfaa6c
p+1  =0xbfdfaa78
sizeof(ma)=24
&ma  =0xbfdfaa6c	r=0xbfdfaa6c
r+1  =0xbfdfaa84
sizeof(ma[0][0])=4
ma[0]=0xbfdfaa6c	t=0xbfdfaa6c
t+1  =0xbfdfaa70

注意在结果中,p,r和t的值均相同,但是所指向的数据却不同。更具体的说,这三个指针每次移动时的偏移量不同。

多维数组的初始化

数组的初始化只能在对数组进行声明(具体为定义型声明)时进行。一维数组的初始化很简单,只要将所有初始值放在一个大括号中即可。如果声明数组时未指定数组的长度,则编译器会根据初始值的个数来确定数组的长度。

#include < stdio.h >

int main()
{
	int m[] = {1,2,3};
	int n[] = {1,2,3,};

	printf("length(m)=%d\n",sizeof(m)/sizeof(m[0]));
	printf("length(n)=%d\n",sizeof(n)/sizeof(n[0]));
	return 0;
}

/* 编译并运行 */
edsionte@edsionte-laptop:~/code/expertC$ gcc init_array.c -o init_array
edsionte@edsionte-laptop:~/code/expertC$ ./init_array
length(m)=3
length(n)=3

注意,在最后一个初始值后面可以继续加一个逗号也可以省略,这并不影响数组的长度。

对于多维数组而言,通常使用嵌套的大括号进行多维数组的初始化。由于多维的数组其实是有若干个一维数组构成的,则每个大括号都代表一个一维数组。对于多维数组而言只能省略最左边 下标的长度。

#include < stdio.h >

int main()
{
	int b[][3] = {1,2,1,1};
	int c[][3] = {{1,2,1},{1,2,3},};

	printf("length(b)=%d\n",sizeof(b)/sizeof(b[0]));
	printf("length(c)=%d\n",sizeof(c)/sizeof(c[0]));
	return 0;
}

/* 编译并运行 */
edsionte@edsionte-laptop:~/code/expertC$ gcc init_array.c -o init_array
edsionte@edsionte-laptop:~/code/expertC$ ./init_array
length(b)=2
length(c)=2

可以看到,不使用大括号也可以对多维数组进行初始化,只不过代码可读性较差。

它总是迷惑你!

一旦涉及到多维数组,总有些让你迷惑的地方。比如:

	char ma[2][3][2]={
		{{1,2},{2,3},{3,4}},
		{{3,5},{4,5},{3,3}}
	};

sizeof(ma[0,1,1])=?

对于上面的代码,我们最后的迷惑点都可能落在ma[0,1,1]上。难道多维数组可以这样使用吗?如果ma[0,1,1]和ma[0][1][1]等价,那么sizeof(ma[0,1,1])的值就是1。很可惜这样的猜测是不正确的,正确答案为6。再比如下面的代码:

		char ma[3][2] = {
		(1,2),(3,4),(5,3)
	};

ma[0][0]=?

上述代码是为数组ma进行初始化,那么ma[0][0]的值是多少?恐怕很多人都会认为是1。不过正确答案是2。

这两个问题都涉及到了逗号表达式。如果你对逗号表达式有基本的了解,那么也就没有上述那种莫名其妙的迷惑了。根据逗号表达式的运算,对于举例1中的ma[0,1,1]实际上等价于ma[1];对于举例2中的初始化其实等价为char ma[3][2] = {2,4,3}。

参考:

《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;

 

指针和数组的可交换性

2011年2月7日

指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。《指针和数组不是一回事儿》系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于《指针和数组不是一回事儿》系列文章之三。

虽然前面两篇文章已经说明了数组和指针的不同,但不可否认的是,指针和数组某些可相互交换的用法仍然令人混淆。本文将给出指针和数组可交换的情景,并且分析可交换的原因。

“指针和数组可以交换!”

说出这句话并不是毫无根据的,因为在下面的两个举例中使用数组形式和指针形式都可以达到相同的结果。

举例1:

#include < stdio.h >

int main()
{
	char *p = "edsionte";
	char str[] = "edsionte";

	printf("p[1]=%c *(p+1)=%c\n",p[1],*(p+1));
	printf("str[1]=%c *(str+1)=%c\n",str[1],*(str+1));

	return 0;
}

/* 编译并运行程序 */
edsionte@edsionte-laptop:~/code/expertC$ gcc tmp.c -o tmp
edsionte@edsionte-laptop:~/code/expertC$ ./tmp
p[1]=d *(p+1)=d
str[1]=d *(str+1)=d

在举例1中,指针p指向一个匿名的字符串“edsionte”,这个匿名字符串的占用的内存空间为9个字节;与p指向一个匿名字符串不同,数组str内存储着字符串“edsionte”,占用了9个字节的空间。

现在分别要访问’d’,则方法如下。对于指针p,分别可以通过指针形式*(p+1)和数组形式p[1]来访问其所指的数据;对于数组str,分别可以通过指针形式*(str+1)和数组形式str[1]来访问数组内的元素。

我们已经知道指针和数组在内存构造和访问方式上都不同,但为什么它们都分别可以通过指针的方式和数组的方式进行访问?

举例2:

#include < stdio.h >

void getStr_pointer(char *str)
{
	printf("%s\n",str);
	printf("getStr_pointer(): sizeof(str)=%d\n",sizeof(str));
}

void getStr_array(char str[100])
{
	printf("%s\n",str);
	printf("getStr_array(): sizeof(str)=%d\n",sizeof(str));
}

int main()
{
	char str[] = "I am edsionte!";

	getStr_pointer(str);
	getStr_array(str);
	printf("main(): sizeof(str)=%d\n",sizeof(str));
}

/* 编译并运行程序 */
edsionte@edsionte-laptop:~/code/expertC$ gcc tmp2.c -o tmp2
edsionte@edsionte-laptop:~/code/expertC$ ./tmp2
I am edsionte!
getStr_pointer(): sizeof(str)=4
I am edsionte!
getStr_array(): sizeof(str)=4
main(): sizeof(str)=15

在举例2中,getStr_pointer函数和getStr_array函数的功能都是显示一条字符串。但不同的是,前者传入的参数是一个指针,后者传入的参数是一个数组。在主函数中分别调用这两个函数,传入的参数都是数组str。

既然数组和指针不同,但为什么作为函数的形参,char str[ ]和char *str相同?

上述举例所引出的这两个问题正是本文讨论的重点,它们分别对应着“指针和数组是相同”的两种情况。下面将分别进行讨论。

1.表达式中的数组名就是指针

表达式中的数组名其实就是数组首元素的首地址。对于编译器而言,a[i]其实就是*(a+i)的形式,因此以数组形式访问数组元素总是可以写成“数组首元素首地址加上偏移量”的形式。取下标符号[ ]其实可以看成一种运算规则,即指向T类型的指针和一个整数相加,最终产生的结果类型为T。这里的指针就为数组首元素首地址,而整数即为数组的偏移量。

这里必须说明一下偏移量,它是指针每次移动的步长。对于数组而言,偏移量即数组元素的大小;对于指针而言,它的偏移量即为指针所指类型的大小。在对指针进行移动时,编译器负责计算每次指针移动的步长。

因此,str[i]和*(str+i)两种形式其实是等价的。因为编译器总是将数组形式的访问自动转换成指针形式的访问。上面的分析都是针对数组而言,其实对指针以数组和指针形式访问的原理也是如此。只不过此时的访问是对指针所指向数据的访问。

结合数组和指针访问方式的不同,下面对举例1的代码做详细分析:

1.1.以指针的形式和以数组的形式访问数组

从符号表中得到符号str的地址即为数组首元素的首地址。

  • 以数组的形式:str[1]。从符号表中得到str符号的地址,即数组首元素的首地址;编译器将数组形式转化为*(str+1),在首元素首地址上加一个偏移量得到新地址;从这个新地址中读取数据,即为’d’;
  • 以指针的形式:*(str+1)。从符号表中得到str的地址,即数组首元素的首地址;在此地址上加一个偏移量得到新地址;从这个新地址中读取数据,即为’d’;

1.2.以指针的形式和以数组的形式访问指针

不管以何种方式访问,我们应该清楚p始终是一个指针。从编译器符号表中得到符号p的地址为指针p的地址。

  • 以指针的形式:*(p+1)。首先从符号表中得到p的地址;从该地址中得到指针p;对指针p加上1个偏移量得到新地址;从这个新地址中读取数据,即为’d’;
  • 以数组的形式:p[1]。首先从符号表中得到p的地址;从该地址中得到指针p;编译器将数组形式转化成*(p+1),对p加一个偏移量得到新地址;从这个新地址中读取新数据,即为’d’;

分析至此,你应该了解到以数组形式和以指针形式访问只是写法上的不同而已,其本质对内存的访问过程是一样的。

2.作为函数参数的数组名等同于指针

当作为函数形参时,编译器会将数组改成指向数组首元素的指针。此时的数组就等价于指针。之所以将传递给函数的数组形参转化为指针是处于效率的考虑。

在C语言中,所有非数组的实参数据都是以传值形式传递给函数的,即将实参的一份拷贝传递给调用函数中的形参,调用函数对这份拷贝(也就是形参)的修改不影响实参本身的值。如果按照这样的道理,传递数组时就必须拷贝整个数组空间,这样必然会产生很大的开销。并且,大部分时候并不会访问到数组中所有的元素而只是其中的几个。考虑到上述的原因,数组作为实参传递给调用函数时,只需将数组名传递给函数即可;而形参会被编译器该成指针的形式。因此,作为形参的数组既可以写成数组也可以写成指针。

现在再回到举例2中的代码,对于形参中的char str[]和char *str也就感到不再奇怪了。事实上,即便将形参写成char str[]或char str[100],编译器仍然会将它们改成char *str的形式。

既然任何数组作为形参时候都等价于一个指针,那么在函数内对“数组”的一切操作都等价于对指针的操作。验证这一点的很好例证就是举例2中对数组str求长度。在主函数中,sizeof(str)的值为15,这个结果毫无争议,它就是数组str的长度。而在getStr_pointer()和getStr_array()中,sizeof(str)的值都为4,也就验证了作为形参的数组str在调用函数中就是一个指针!在上述情况1中,虽然表达式中数组名也被认为是指针,但是数组仍然是数组(main函数中sizeof的结果就是很好的验证),而此部分数组就是指针。这也是数组等价于指针的唯一情况。

换句话说,虽然在将数组作为形参的函数中,你可以继续以数组的形式使用这个参数,但实际上你跟不可能找到数组的踪影!

总结

关于指针和数组之间的异同需要反复的思考和总结,才能搞清关系。下面对指针和数组之间的可交换性再作义简单的总结。

1.在表达式中以a[i]这样的形式对数组进行访问时,编译器总将其解释为*(a+i)的形式;

2.在数组作为函数的形参时,编译器将数组改写成指针,这个指针即为数组首元素的首地址。这也是数组等价指针的唯一情形;

3.由于2的原因,一个数组作为函数的形参时,既可以将数组定义成数组,也可以将数组定义成指针;

4.指针和数组永远是两码事,因此在不同文件中的声明和定义必须匹配,但却始终都能写成指针的形式和数组的形式(这完全是写法的不同)。

参考:

《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;

《C语言深度解剖》北京航空航天大学出版社;陈正冲 著;

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key