本来这一讲是打算讲指针的,可是考虑到C++中指针的更多操作,我不想讲一个问题分成两部分,因此,我就先讲C++的部分,等需要用到指针的时候,我们专门写一个专题讲述指针部分。 好进入正题。
大家都知道,C++是在C的基础上扩展了好多东西,其中好多是思想上的转变,因此,很多C++中的东西,我们都可以用C语言来模拟出来,比如构造、析构等等。 但是也有很多是C++编译器扩展的东西,我们没有办法用C去模拟,因此,我写了这个小节,重在理解……
C++中,零散的知识点比较多,因此,我每个专题尽量减少其内容,而增加更新频率,希望大家能有助于大家的理解。
一、宏定义的扩展——const、inline。
#define 宏定义,是C的知识范畴,由于全都都语法范畴且又仅仅是简单的查找替换因此我没有为它单独的列一个专题,大家可以自己学习一下。我想如果要讲这个内容,也得等我们课程进行到MFC或者WTL的时候再捎带提一下。
学习过#define 以后,我们知道,它一般用来定义常量,由于它可以带参数,且因没有函数调用时传参的复杂过程而速度快的优点,因此它经常被当作“函数”使用。
但是#define也是有缺点的,比如它定义的常量没有类型信息,它定义的“函数”(带参宏)不安全等原因,C++对这个功能进行了扩充,分别用const和inline两个关键字来分别代替无参宏定义常量,带参宏定义函数。下面分别看一下它们的用法及原理。
1. const 的用法。
const用于定义常量,基础语法如下:
const 类型 常量名
类型 const 常量名
关于const的基础语法:const 只是对除类型外,紧靠其右边的元素。
比如:
// 这时const右面除了类型就是nCountNum变量,所以,nCountNum的内容不可以改变。
const int nCountNum = 5;
// const右边是*,在指针中*表示内容,所以pnCountNum指针指向的变量内容不可以被改变。
const int *pnCountNum = nCountNum;
// const右边还是*,所以同上
int const *pnCountNum = nCountNum;
// const右边是指针变量名,指针变量代表地址,所以pnCountNum中的内容不可以被改变。
int * const pnCountNum = nCountNum;
OK,知道了以上的语法知识,我们需要了解,这种常量跟我们用宏定义的常量是有区别的,因为这个是编译器级别的常量,我们可以通过指针来修改它的内容,当然,若在代码中使用const变量,则直接在代码中使用其常量值,如下面的代码:
const int nCount = 50;
int tmpNum = 0;
scanf("%d", &tmpNum); // 防止编译器自动优化代码
printf("%d", tmpNum + nCount);
// 26: printf("%d", tmpNum + nCount);
// 0040D427 mov ecx,dword ptr [ebp-8]
// 0040D42A add ecx,32h // 直接当作常量使用
// 0040D42D push ecx
// 0040D42E push offset string "%d" (0042201c)
// 0040D433 call printf (0040f900)
通过上面的代码,我们可以知道,nCount这个常量有自己的栈地址,只要有地址,我们肯定是可以用指针来修改它内容的。
但是,通过
// 0040D42A add ecx,32h // 直接当作常量使用
这一句,我们清楚,这个代码是直接将nCount 当做常量使用,所以即使我们更改了nCount 中的内容,这里的值也不会改变了。
2. inline 的用法。
是的,虽然带参宏的使用提高了编码的效率,从一定程度上提高了程序的运行效率(因为它少了函数调用的压栈出栈等操作)而被MFC,WTL等广泛的应用,但是不可否认用带参宏的不安全性,在C++中引入了inline函数的概念,它用inline函数来代替带参宏的功能。
到这里,我们就不难理解,inline函数的特点了:在函数被调用的地方将代码展开。比如下面的代码:
/************************************************************************/
/* 在C++中用内联函数来代替有参宏,跟有参宏一样,它在调用的地方原地展开
/* 因此,内联函数也同有参宏一样,在同一个代码中存在多份拷贝。
/* 所以,内联函数一般声明在头文件中就可以了。
/* 说 明:
/* 内联函数中不能包含switch、while等复杂结构,如果逻辑复杂了编译器
/* 就将它当做普通函数处理。
/************************************************************************/
inline int add(int a, int b, int c)
{
return a+b+c;
}
int main(int argc, char* argv[])
{
int a,b,c;
scanf("%d %d %d", &a, &b, &c);
printf("%d", add(a,b,c));
return 0;
}
由于DEBUG方式编译的程序不做任何优化,所以,我们release方式编译此代码,得到如下信息:
/*DEBUG 模式下,内联就是普通函数,release模式下才真正的内联。*/
// 00401000 >/$ 83EC 0C sub esp, 0xC ; _main
// 00401003 |. 8D4424 08 lea eax, dword ptr [esp+0x8]
// 00401007 |. 8D4C24 04 lea ecx, dword ptr [esp+0x4]
// 0040100B |. 8D5424 00 lea edx, dword ptr [esp]
// 0040100F |. 50 push eax
// 00401010 |. 51 push ecx
// 00401011 |. 52 push edx
// 00401012 |. 68 34804000 push offset <??_C@_08NNKG@?$CFd?5?$CF>; ASCII "%d %d %d"
// 00401017 |. E8 55000000 call <_scanf>
// 0040101C |. 8B4424 10 mov eax, dword ptr [esp+0x10]
// 00401020 |. 8B4C24 14 mov ecx, dword ptr [esp+0x14]
// 00401024 |. 03C1 add eax, ecx
// 00401026 |. 8B4C24 18 mov ecx, dword ptr [esp+0x18]
// 0040102A |. 03C1 add eax, ecx
// 0040102C |. 50 push eax
// 0040102D |. 68 30804000 push offset <??_C@_02MECO@?$CFd?$AA@> ; ASCII "%d"
// 00401032 |. E8 09000000 call <_printf>
// 00401037 |. 33C0 xor eax, eax
// 00401039 |. 83C4 24 add esp, 0x24
// 0040103C \. C3 retn
Inline函数的代码被展开贴到main函数中的,是吧……
当然,并不是所有的代码都可以被写程序inline函数的,它有如下几点要求:
1、 inline函数中的代码逻辑不可过于复杂
2、 不能包含如循环,switch等复杂的语句
否则,编译器会将inline函数当做一个普通函数处理。
二、 指针与引用。
我想,虽然我没有系统的讲过指针,但是根据我们第一课中的内容的提示,我相信,大家一定能够理解指针的概念。所以我这一节不详细讲述指针的概念,
引用,是C++提出来的一个新的概念,不多废话,看代码:
/************************************************************************/
/* 引用是C++新增加的运算符。
/* 基本用法如下:
/************************************************************************/
void BaseUse()
{
printf("/-----------基础用法---------------/\r\n");
int nBuf = 0;
scanf("%d", &nBuf); // 防止编译器自动优化
int &a1 = nBuf; // 引用的用法
int *pa1 = &nBuf; // 指针是变量的地址
printf("%d %d %d\r\n", nBuf, a1, *pa1);
}
上面代码我们给出了引用的最基本的用法,为了我们能够将它与指针加以区别,我跟一指针一起使用,然后我们通过分析它的反汇编代码,我们给出他们的区别。
由于Release模式下开了O2选项,对代码进行了优化,所以我们看DEBUG模式的代码
0040D77F |. 8D45 FC lea eax, dword ptr [ebp-0x4]
0040D782 |. 50 push eax
0040D783 |. 68 1C204200 push 0042201C ; /format = "%d"
0040D788 |. E8 D3210000 call scanf ; \scanf
0040D78D |. 83C4 08 add esp, 0x8
0040D790 |. 8D4D FC lea ecx, dword ptr [ebp-0x4] ; 给引用赋值
0040D793 |. 894D F8 mov dword ptr [ebp-0x8], ecx
0040D796 |. 8D55 FC lea edx, dword ptr [ebp-0x4] ; 指针的用法
0040D799 |. 8955 F4 mov dword ptr [ebp-0xC], edx
0040D79C |. 8B45 F4 mov eax, dword ptr [ebp-0xC] ; 取内容
由此,我们知道,引用时当作指针使用的,他们的传递方式一摸一样,只是,引用在操作的时候,多了一个取内容的操作。
我们得出结论如下:
引用就是指针取内容。
指针就是引用取地址。
当然,如果我的这节课程到这里就结束了,似乎有点对不住各位同学,因为我这里似乎只讲述了语法的东西,下面呢,我由引用的话题,讲一下引用的一些高级用法。
// 定义一个学生信息的结构体
typedef struct _DATA_STUDENT_INFO
{
int nID;
char *szName;
char chsex;
}DATA_STUDENT_INFO, *PDATA_STUDENT_INFO;
// 声明一个全局变量
DATA_STUDENT_INFO g_DSI[2] = {0};
// 返回一个结构体的引用
DATA_STUDENT_INFO& GetStudentObj(int nIndex)
{
return g_DSI[nIndex];
}
int ExtendUse()
{
printf("/-----------扩展用法---------------/\r\n");
GetStudentObj(0).chsex = 1;
GetStudentObj(0).szName = "besterChen";
GetStudentObj(0).nID = 1;
printf("I D: %d\r\n", GetStudentObj(0).nID);
printf("Name: %s\r\n", GetStudentObj(0).szName);
printf(" Sex: %d\r\n", GetStudentObj(0).chsex);
return 0;
}
由于引用时指针取内容,所以,GetStudentObj返回的就是一个对象,因此可以直接对它的成员进行操作。
三、 学习小结
我临时决定讲C++的,所以指针的专题等后期再讲,因为一来我不想让指针的话题分成两部分讨论,二来我还没有准备好讲指针将的透彻(其实主要就是没有信心)。
另外,我觉得,我们现在打下的那些基础足够我们学C++了,虽然好多朋友都说:没有必要学习C语言,直接学C++即可,但是我始终相信学习C语言是有必要的,因为C语言着重内存结构(好让我们把握住程序的本质),C++着重于设计思想(也有好多的语法知识要学)。
直到上个专题内存操作,我们几乎把C语言都讲完了(当然仅仅是让人迷糊的关键部分),本专题是C++课程的开始,我也着重于内存,因为我始终相信,掌握了内存结构就掌握了编程的本质。
如果一直读我的破烂文章到现在的朋友一定会发现,这个系列不是给没有一点基础的朋友准备的,也不是为了讲述语法知识的,所以,我更新的很慢,这个更新速度足矣让大家补好自己的基础。
所以,C++部分我会尽快的更新,因为大家都有基础了。
最后,祝大家成功。