我们在学习C++的时候,有很多人不知怎样学习它、怎样学好它,也不知应先从那里开始学起,关于C++的入门其实很简单,你只要一步步按照你手中的那本C++教材来学就可以了,也许你把C++学的很烂,这时你千万不要认为我好像很精通C++了。
我个人认为要想学习C++,最好直接学习它,不要先学习C语言,然后在学习C++,虽然C++是从C语言上发展过来的,但如果你对C语言了解的越
多,在你写C++程序的时候,你很难摆脱C的风格,既使你是一位很有经验的程序员,如果你对C很了解,在学习C++的时候,尽量使用C++的风格,我这样
并不是说C不好,关键我们现在要了解的是C++而不是C。
现在让我们深入学习C++吧!C++的难学,不仅在它那广博的语法、语法背后的语义、语义背后的深层思维、深层思维背后的对像模型;C++的难
学,还在与它提供了四种不同的编程思维模型。当我们找来一本C++教材时,当我们翻开第一页时,这时我们已进入了C++的世界,我们现在开始探索,开始在
追求新技术的旅程中!
想学好C++,熟练掌握它的语法是不可少的,当你掌握了C++的语法时,那么我要恭喜你,你已正正进入了C++的世界,要想学好C++,你只有努
力的学习,经常的思考多多的实践,这时你会问了,我应该还要学习什么呢?
C++的语法我都已掌握了啊!我是不是可以学习Windows编程了呢?不要急,你是已掌握了C++的语法,但你能用它写出高效率的程序吗?你已对C++
所有运行机制都了解吗?是的,单单了解C++语法是不够的,接下来你的任务很多,要学习如何高效地使用C++语言。现在我就教你怎样的学好它,怎样的高效
使用它。
我们还是先从C++的语法开始说起吧!这里我只做一个简单的概述,当我们学习C++的时候,你先要了解它的编程模式,其中包括面向对像编程、通用
编程和传统的过程化编程。当你在学习一个C++语法时,如果你一时感到很难理解,不妨你先跳过这一段,继续向后学习,当你看完你所学习C++的那本教材
时,你在回过头来学习C++,你会发现其实它就是那么回事,有很多人在学习C++时,刚学习到了一半,突然感到好像以前学习的语法忘了许多,他们会把书又
翻回去,找回那忘掉的语法,如果你在学习C++时也有这样的情况,你大可不必那么担心,你现在的任务是继续你的学习,不要去管那一时不记得的语法,如果你
现在去重新学习那一时忘掉的C++,恩,不错,这看起来你好像对那语法已深深的牢记在心,当你的C++在学习到这里时,你能保证前面的语法不在遗忘吗?这
时的你在学习新的C++语法时,但心会忘掉前面刚刚找回的C++,你说这时你能学好新的C++语法吗?你会一边学习新的,一边重复旧的,这样一来,那就糟
了,这时的你会很容易搞乱新旧C++语法,新的记不住,旧的又被新的语法搞乱了,这时的你不得不从头再来(毕竟你是初学者)。
对于初学者来说,C++的广博语法是件头疼的事,学会了这个却忘了那个,就像我上面提到的那样,这时的你应该继续的学习C++新知识,等看完你手
中的那本C++教材时,你在来学习忘掉的语法,这时你会感觉好像C++很简单,没有我们开始说的那么难学啊!你会觉得我开始说C++难学是用来吓唬人的。
我说C++难学当然不是用来吓唬人的,这时的你对C++语法已非常熟悉了,这时你千万不要认为对C++已很精通,就像我开头所说的那样,虽然现在你已摆脱
了初学着的称呼,但你也不能算是位精通人士啊!你只掌握了C++的大概,接下来的你就要深入学习拉!
本文的写作目的并不在于提供C/C++程序员求职面试指导,而旨在从技术上分析面试题的内涵。文中的大多数面试题来自各大论坛,部分试题解答也参考了网友的意见。
许多面试题看似简单,却需要深厚的基本功才能给出完美的解答。企业要求面试者写一个最简单的strcpy函数都可看出面试者在技术上究竟达到了怎样的程
度,我们能真正写好一个strcpy函数吗?我们都觉得自己能,可是我们写出的strcpy很可能只能拿到10分中的2分。读者可从本文看到strcpy
函数从2分到10分解答的例子,看看自己属于什么样的层次。此外,还有一些面试题考查面试者敏捷的思维能力。
分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。
2.找错题
试题1:
void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
试题2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1
= 'a';
}
strcpy( string, str1 );
}
试题3:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
试题1字符串str1需要11个字节才能存放下(包括末尾的’\0’),而string只有10个字节的空间,strcpy会导致数组越界;
对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string,
str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10
分;
对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计’\0’所占用的1个字节。
剖析:
考查对基本功的掌握:
(1)字符串以’\0’结尾;
(2)对数组越界把握的敏感度;
(3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案:
2分
void strcpy( char *strDest, char *strSrc )
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
4分
void strcpy( char *strDest, const char *strSrc )
//将源字符串加const,表明其为输入参数,加2分
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
7分
void strcpy(char *strDest, const char *strSrc)
{
//对源地址和目的地址加非0断言,加3分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
10分
//为了实现链式操作,将目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc )
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ );
return address;
}
从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!
(4)对strlen的掌握,它没有包括字符串末尾的'\0'。
读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const
{
assert( strt != NULL ); //断言字符串地址非0
int len;
while( (*str++) != '\0' )
{
len++;
}
return len;
}
试题4:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str );
strcpy( str, "hello world" );
printf( str );
}
试题5:
char *GetMemory( void )
{
char p[] = "hello world";
return p;
}
void Test( void )
{
char *str = NULL;
str = GetMemory();
printf( str );
}
试题6:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" );
printf( str );
}
试题7:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str );
... //省略的其它语句
}
解答:
试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完
char *str = NULL;
GetMemory( str );
后的str仍然为NULL;
试题5中
char p[] = "hello world";
return p;
的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句 tiffany
bracelets
*p = (char *) malloc( num );
后未判断内存是否申请成功,应加上:
if ( *p == NULL )
{
...//进行申请内存失败处理
}
试题7存在与试题6同样的问题,在执行
char *str = (char *) malloc(100);
后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上:
str = NULL;
试题6的Test函数中也未对malloc的内存进行释放。
剖析:
试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
再看看下面的一段程序有什么错误:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为:
swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}
我们现在编写一个程序越来越容易了。利用一些软件开发工具,往往只要通过鼠标的拖拖点点,计算机就会自动帮你生成许多代码。但在很多时候,计算机的这种能力被滥用了,我们往往只考虑把这个程序搭起来,而不去考虑程序的性能如何,程序是否足够的健壮。而此节课的目的主要是介绍一些编码的经验,让大家编写的程序更加健壮和高性能。
在C++编程中应该尽量使用const和inline来代替#define,尽量做到能不用#define就不用。#define常见的用途有“定义常量”以及“定义宏”,但其中存在诸多的弊病。
第一,查错不直观,不利于调试。Define的定义是由预处理程序处理的,作的是完全的文本替换,不做任何的类型检查。在编译器处理阶段,define定义的东西已经被完全替换了,这样在debug的时候就看不到任何的相关信息,即跟踪时不能step into宏。例如,把ASPECT_RATIO用define定义成1.653,编译器就看不到ASPECT_RATIO这个名字了。如果编译器报1.653错,那么就无从知道此1.653来自于何处。在真正编码的时候应该使用如下的语句来定义:
static const double ASPECT_RATIO = 1.653;
第二,没有任何类型信息,不是type safe。因为它是文本级别的替换,这样不利于程序的维护。
第三,define的使用很容易造成污染。比如,如果有两个头文件都定义了ASPECT_RATIO, 而一个CPP文件又同时包含了这两个头文件,那么就会造成冲突。更难查的是另外一种错误,比如有如下的代码:
// in header file def.h
#define Apple 1
#define Orange 2
#define Pineapple 3
…
// in some cpp file that includes the def.h
enum Colors {White, Black, Purple, Orange};
在.h文件中Orange被定义成水果的一种,而在.cpp文件中Orange又成为了一种颜色,那么编译器就会把此处的Orange替换成2,编译可能仍然可以通过,程序也能够运行,但是这就成了一个bug,表现出古怪的错误,且很难查错。再比如定义了一个求a与b哪个数大的宏,#define max(a,b) ((a) > (b) ? (a) : (b))
int a = 5, b = 0;
max(++ a, b);
max(++ a, b + 10);
190-823 117-202 在上面的操作中,max(++ a, b); 语句中a被++了两次,而max(++ a, b + 10); 语句中a只加了一次,这样在程序处理中就很有可能成为一个bug,且此bug也非常的难找。在实际编码时可以使用如下的语句来做:
template<class T>
inline const T&
max(const T& a, const T& b) { return a > b ? a : b; }
2、Prefer C++-style casts 在程序中经常会需要把一种类型转换成另外一种类型,在C++中应该使用static_cast、const_cast、dynamic_cast、reinterpret_cast关键字来做类型转换。因为这有以下好处,一是其本身就是一种注释,在代码中看到上面这些关键字就可马上知道此处是进行类型转换。二是C语言中类型转换通常是很难进行搜索的,而通过关键字cast则可以很容易的找到程序中出现类型转换的地方了。
3、Distinguish between prefix and postfix forms of increment and decrement operators 通常对于操作系统或编译器自身支持的类型,prefix(前缀,如++i)与postfix(后缀,如i++)的效果是一样的。因为现在的编译器都很聪明,它会自动做优化,这两者的汇编代码是一样的,性能不会有差别。但有时候也会有不同的,如一些重载了操作符的类型。下面是模拟prefix与postfix的操作过程,可以发现在postfix操作中会生成一个临时变量,而这一临时变量是会占用额外的时间和开销的。
// prefix form: increment and fetch
UPInt& UPInt::operator++()
{
*this += 1; // increment
return *this; // fetch
}
// postfix form: fetch and increment
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this; // fetch
++(*this); // increment
return oldValue; // return what was fetched
}
一般情况下不需要区分是先++,还是后++,但是我们在编写程序的时候最好能习惯性的将其写成++i的形式,如在使用STL中的iterator时,prefix与postfix会有相当大的性能差异。请不要小看这些细节,实际在编写程序的时候,若不注意具体细节,你会发现程序的性能会非常的低。但要注意,虽然在大多数情况下可以用prefix来代替postfix,但有一种情况例外,那就是有[]操作符时,比如gzArray [++index] 是不等于 gzArray[index++]的。
4、Minimizing Compile-time Dependencies
有些人在编写程序时,往往喜欢将一个.h文件包含到另一个.h文件,而实践证明在做大型软件时这是一个非常不好的习惯,因这样会造成很多依赖的问题,包含较多的.h文件,别人又使用了这个class,而在他的那个工程中可能并不存在这些.h文件,这样很可能就编译不能通过。而且这样做,还可能造成很难去更新一个模块的情况。因为一个.h文件被很多模块包含的话,如果修改了此.h文件,在编译系统的时候,编译器会去寻找哪些模块依赖于某个被修改过的.h文件,那么就导致了所有包含入此.h文件的模块全都要进行重新编译。在项目比较小的时候,大家可能还感觉不到差别,但是如果说是在大型的软件系统里,你可能编译一遍源码需要七、八个小时。如果你这个.h文件被很多模块包含的话,就算在.h文件中加了一行注释,在编译时编译器检查哪些文件被改动,那么所有包含入此.h文件的模块都会被重新编译,造成巨大的时间和精力负担。对于此问题,解决的方法就是让.h文件自包含,也就是说让它包含尽量少的东西。所谓尽量少是指如删掉任何一个它包含进来的.h文件,都将无法正常进行工作。其实在很多情况下,并不需要一个.h文件去包含另一个.h文件,完全可以通过class声明来解决依赖关系的这种问题。再来看下面这个例子:1Z0-043
#include "a.h" // class A
#include "b.h" // class B
#include "c.h" // class C
#include "d.h" // class D
#include "e.h" // class E
class X : public A, private B
{
public:
E SomeFunctionCall(E someParameter);
private:
D m_dInstance;
};
当类X从类A和类B中派生时,需要知道X在内存中都有哪些data,通常在内存中前面是基类的data,后面紧跟的是此派生类自身定义的data,因此就必须知道类A与类B的内部细节,要不然编译器就无法来安排内存了。但是在处理参数以及参数返回值的时候,实际上并不需要知道这些信息,在此处定义的SomeFunctionCall()只需知道E是个class就足够了,并不需要知道类E中的data如长度等的具体细节。上面的代码应该改写成如下的形式,以减少依赖关系:
#include "a.h" // class A
#include "b.h" // class B
#include "c.h" // class C
#include "d.h" // class D
class E;
class X : public A, private B
{
public:
E SomeFunctionCall(E someParameter);
private:
D m_dInstance;
};
5、Never treat arrays polymorphically
不要把数组和多态一起使用,请看下面的例子。
class BST { ... };
class BalancedBST: public BST { ... };
void printBSTArray(ostream& s, const BST array[], int numElements)
{
for (int i = 0; i < numElements; ++i)
{
s << array[i];
// this assumes an operator<< is defined for BST
}
}
BalancedBST bBSTArray[10];
printBSTArray(cout, bBSTArray, 10);
数组在内存中是一个连续的内存空间,而在数组中应该如何来定位一个元素呢?过程是这样的,编译器可以知道每个数据类型的长度大小,如果数组的index是0,则会自动去取第一个元素;如果是指定了某个index,编译器则会根据此index与该数据类型的长度自动去算出该元素的位置。
在printBSTArray()函数中,尽管传入的参数是BalancedBST类型,但由于其本来定义的类型是BST,那么它依然会根据BST来计算类型的长度。而通常派生类实例所占的内存要比基类实例所占的内存大一些,因此该程序在编译时会报错。请记住,永远不要把数组和C++的多态性放在一起使用。
6、Prevent exceptions from leaving destructors
析构函数中一定不要抛出异常。通常有两种情况会导致析构函数的调用,一种是当该类的对象离开了它的域,或delete表达式中一个该类对象的指针,另一种是由于异常而引起析构函数的调用。
如果析构函数被调用是由于exception引起,而此时在析构函数中又抛出了异常,程序会立即被系统终止,甚至都来不及进行内存释放。因此如果在析构函数中抛出异常的话,就很容易混淆引起异常的原因,且这样的软件也会让用户非常恼火。由于析构函数中很可能会调用其它的一些函数,所以在写析构函数的时候一定要注意,对这些函数是否会抛出异常要非常清楚,如果会的话,就一定要小心了。比如下面这段代码:
Session::~Session()
{
logDestruction(this);
}
比如logDestruction()函数可能会抛出异常,那么我们就应该采用下面这种代码的形式:
Session::~Session()
{
try
{
logDestruction(this);
}
catch (...)
{
}
}
这样程序出错的时候不会被立即关掉,可以给用户一些其它的选择,至少先让他把目前在做的工作保存下来。
7、Optimization:Remember the 80-20 rule
在软件界有一个20-80法则,其实这是一个很有趣的现象,比如一个程序中20%的代码使用了该程序所占资源的80%;一个程序中20%的代码占用了总运行时间的80%;一个程序中20%的代码使用了该程序所占内存的80%;在20%的代码上面需要花费80%的维护力量,等等。这个规律还可以被继续推广下去,不过这个规律无法被证明,它是人们在实践中观察得出的结果。从这个规律出发,我们在做程序优化的时候,就有了针对性。比如想提高代码的运行速度,根据这个规律可以知道其中20%的代码占用了80%的运行时间,因此我们只要找到这20%的代码,并进行相应的优化,那么我们程序的运行速度就可以有较大的提高。再如有一个函数,占用了程序80%的运行时间,如果把这个函数的执行速度提高10倍,那么对程序整体性能的提高,影响是非常巨大的。如果有一个函数运行时间只占总时间的1%,那就算把这个函数的运行速度提高1000倍,对程序整体性能的提高也是影响不大的。所以我们的基本思想就是找到占用运行时间最大的那个函数,然后去优化它,哪怕只是改进了一点点,程序的整体性能也可以被提高很多。
要想找出那20%的代码,我们的方法就是使用Profiler,它实际上是一些公司所开发的工具,可以检查程序中各个模块所分配内存的使用情况,以及每个函数所运行的时间等。常见的Profiler有Intel公司开发的VTune,微软公司开发的Visual Studio profiler,DevPartner from Compuware等
首先说指导思想。这是一个价值观问题,我们在此提出三条标准:简单,高性能,可移植。
我们在开篇就对简单性目标作了叙述,这里再稍微展开讨论一下。我们提出的简单标准,首先是外部接口简单,其次是内部结构简单。我们知道,类库是提供给上层应用程序使用的,也就是按照一定的接口规范,向上层提供一定的功能服务。接口设计得越简单,对上层用户来说就越方便,就越不容易产生Bug。我们可以注意到,流行的成功类库都是拥有简单接口的。为了使接口简单,常常不得不把有关具体实现的复杂性封装于类库内部,也就是说,关于简单性的设计原则,外部接口简单优先于内部实现简单。
高性能是C++语言优于其它OO语言的一个特性。C++的高性能应该首先归于它运行模式,和大多数OO语言不同,C++程序编译后直接产生本地平台代码(Native Code),理论上具备了可能的最大执行性能。另外的一个原因是主流的C++编译器都被设计得非常精巧,具有优越的代码优化能力。对于C++类库设计者来说,保持C++的高性能是一个重要目标。程序的高性能可以从两方面来评价,一是时间性能,以尽量短的时间来解决尽量多的业务;二是资源性能,以尽量少的资源消耗,包括CPU使用、内存占用、网络流量、磁盘空间等等,来维持正常的程序功能。提高性能的主要手段是数据结构、算法和程序体现结构的优化设计000-861 117-102 。
再说可移植性。C++的编译后输出代码是本地平台代码,因此C++本身不具有目标代码可移植性,C++的可移植性只能是源代码可移植性。源代码的可移植性是指,同一软件产品的全部或者部分源代码可以在不同的编译环境中进行编译(不需要编译的除外),并且其结果具有相同的品质特性(依优先顺序包括功能性、可靠性、可用性、性能性、可维护性等)。编译环境可以大致分为三个层次,最底层的是操作系统,也就是平台(Platform),其次是对源代码直接进行处理的编译器,然后是其它在编译过程中必需的中间件物品,如库文件等。我们知道C++虽然在语言规范上获得了统一(ISO/IEC),其编译器却是群雄割据的局面,具有代表性的有Borland C++系列(已经淡出市场),Microsoft的Visual Studio系列的C++编译器和GNU阵营的压轴产品gc中的g++。源代码经编译环境处理后产生的可执行代码的执行平台称为目标平台,不同的编译器的目标平台也不同,有的支持多平台,如g++,有的是单一平台,如Visual C++。对于类库设计者来说,想要获得完全的可移植性是非常困难的(除非是象STL这样被纳入语言规范的类库,因为不支持STL就是不支持标准的C++。即便如此不同的编译环境还是存在不同的STL实现版本,造成“一个类库多个实现”的局面),我们只能有选择地支持一部分环境。我们在开篇就已经说明,我们选择g++和Visual C++编译器,选择Linux和Windows 32位目标平台。
接下来我们来讨论C++类库设计的方法论。
首先,我们采用仅用头文件的类库设计方式(Header-only,STL的大多数实现版本都是采用Header-only的方式),也就是在头文件(.h)中声明和定义类,将其成员函数全部定义为内联函数,而不使用源程序文件(.cpp)。
我们知道在C语言的开发环境中,所谓库文件包含两个部分,头文件部分和二进制文件部分。根据二进制文件和用户目标文件结合方式的不同,又可分为静态链接文件和动态链接文件。这种库的构成模式已成为事实上的C语言开发环境的标准,绝大多数平台、绝大多数编译器都使用这种模式 117-301 190-721 。
然而C++语言开发环境,这种库构成模式遭遇到一个重大问题,就是符号命名问题。举例来说,C++允许多个函数可以被重载(Overload),可以具有相同的名称,而通过参数列表不同被予以区别。这样就带来一个问题,编译完成的目标代码中怎样来区别这些在源代码中具有相同名称的函数?常见的做法是在编译器输出的函数的符号名称中加入描述类型信息的字符串,这种方法通常被称为名称装饰(Name decoration)或者名称糟化(Name mangling,这个术语真不好翻译,笔者的感觉是发明这个词的人觉得编译器把本来简单干净的符号给搞乱了)。比如说,g++3.4.4对于函数void func(int),其编译输出符号名称为_Z1funci,对于函数void func(int, int),其输出符号名称为_Z1funcii,等等。但是,这种名称装饰规则没有统一规范,也就是说不同的编译器有各自不同的名称装饰规则,这样就导致不同的C++编译器只能识别自己的输出文件,而没有办法处理其他编译器的输出文件。因此,如果将C++程序制作成二进制的库文件,则其能够支持的开发环境只能限于原始的开发环境,基本上不具有多种开发环境间的通用性。
一个解决办法是将库文件保持在源代码形态(包括头文件和源文件),而不编译成二进制文件。比如STL的许多实现版本都是以头文件形式存在。这样虽然解决了名称装饰所带来的不可移植问题,但同时又会带来代码编译时间增长,源代码完全公开等问题。在C++的名称装饰规则未被统一之前,看起来这个问题是很难两全其美地解决了。
在本系列中,我们也仿照g++的STL实现方式,完全以头文件形式来编写类库。为什么不把代码放到源文件中去呢?主要原因是,头文件只需要用户使用包含指令(#include)就可以处理了,而源文件则需要配置到用户工程的编译目标列表中,和用户的源程序形成共同编译的形式,破坏了用户工程的编译目标的封闭性,比较麻烦而且不符合软件开发的一般习惯。
其次我们来讨论如何支持多平台。我们已经说过在本系列中我们的线程库支持Linux平台的Posix线程和Windows 32位平台的线程模式。我们可以参考C++的Pimpl“惯语”(Pimpl idiom,在Herb Sutter的《Exceptional C++》中有介绍),采用2层类构造方式。上次类亦即接口类,为用户提供统一的类接口,在用户看来具有唯一的类行为定义;下层类亦即实现类,将接口类的行为定义转化为某个平台的具体实现。
1.引言
C++语言的创建初衷是“a better
C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。
2.从标准头文件说起
某企业曾经给出如下的一道面试题:
面试题
为什么标准头文件都有类似以下的结构?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef
__cplusplus
extern "C" {
#endif
/*...*/
#ifdef
__cplusplus
}
#endif
#endif /* __INCvxWorksh
*/
分析 显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define
__INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。
那么
#ifdef __cplusplus
extern "C" {
#endif
#ifdef
__cplusplus
}
#endif
的作用又是什么呢?我们将在下文一一道来。
3.深层揭密extern
"C"
extern "C"
包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。
被extern
"C"限定的函数或变量是extern类型的;
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
extern int
a;
仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern
“C”修饰。
被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;
未加extern
“C”声明时的编译方式 首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y
);
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled
name”)。
_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void
foo( int x, int y )与void foo( int x, float y
)编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加extern
"C"声明时的连接方式 假设在C++中,模块A的头文件如下:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo(
int x, int y );
#endif
在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);
实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!
加extern
"C"声明后的编译和连接方式
加extern "C"声明后,模块A的头文件变为:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C"
int foo( int x, int y );
#endif
在模块B的实现文件中仍然调用foo( 2,3
),其结果是:
(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A中函数声明了foo为extern
"C"类型,而模块B中包含的是extern int foo( int x, int y )
,则模块B找不到模块A中的函数;反之亦然。
所以,可以用一句话概括extern
“C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):
实现C++与C及其它语言的混合编程。
明白了C++中extern
"C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
4.extern
"C"的惯用法
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern
"C"时会出现编译语法错误。
笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define
C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c
*/
#include "cExample.h"
int add( int x, int y )
{
return x +
y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include
"cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C"
{ }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern
"C"的该头文件,应该仅将C文件中将C++中定义的extern
"C"函数声明为extern类型。
笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:70-210 1Y0-327
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define
CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件
cppExample.cpp
#include "cppExample.h"
int add( int x, int y
)
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include
"cExample.h" */
extern int add( int x, int y );
int main( int argc, char*
argv[] )
{
add( 2, 3 );
return 0;
}
如果深入理解了第3节中所阐述的extern
"C"在编译和连接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。对第4节给出的示例代码,需要特别留意各个细节。
1.把C++当成一门新的语言学习(和C没啥关系!真的。);
2.看《Thinking In
C++》,不要看《C++变成死相》;
3.看《The C++ Programming Language》和《Inside The C++
Object Model》,不要因为他们很难而我们自己是初学者所以就不看;
4.不要被VC、BCB、BC、MC、TC等词汇所迷惑——他们都是集成开发环境,而我们要学的是一门语言;
5.不要放过任何一个看上去很简单的小编程问题——他们往往并不那么简单,或者可以引伸出很多知识点;
6.会用Visual
C++,并不说明你会C++;
7.学class并不难,template、STL、generic
programming也不过如此——难的是长期坚持实践和不遗余力的博览群书;
8.如果不是天才的话,想学编程就不要想玩游戏——你以为你做到了,其实你的C++水平并没有和你通关的能力一起变高——其实可以时刻记住:学C++是为了编游戏的;
9.看Visual C++的书,是学不了C++语言的;
10.浮躁的人容易说:XX语言不行了,应该学YY;——是你自己不行了吧!?
11.浮躁的人容易问:我到底该学什么;——别问,学就对了;
12.浮躁的人容易问:XX有钱途吗;——建议你去抢银行;
13.浮躁的人容易说:我要中文版!我英文不行!——不行?学呀!
14.浮躁的人容易问:XX和YY哪个好;——告诉你吧,都好——只要你学就行;
15.浮躁的人分两种:a)只观望而不学的人;b)只学而不坚持的人;
16.把时髦的技术挂在嘴边,还不如把过时的技术记在心里;
17.C++不仅仅是支持面向对象的程序设计语言;
18.学习编程最好的方法之一就是阅读源代码;
19.在任何时刻都不要认为自己手中的书已经足够了;
20.请阅读《The Standard C++
Bible》(中文版:标准C++宝典),掌握C++标准;
21.看得懂的书,请仔细看;看不懂的书,请硬着头皮看;
22.别指望看第一遍书就能记住和掌握什么——请看第二遍、第三遍;
23.请看《Effective C++》和《More
Effective C++》以及《Exceptional C++》;
24.不要停留在集成开发环境的摇篮上,要学会控制集成开发环境,还要学会用命令行方式处理程序;
25.和别人一起讨论有意义的C++知识点,而不是争吵XX行不行或者YY与ZZ哪个好;
26.请看《程序设计实践》,并严格的按照其要求去做;
27.不要因为C和C++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样;
28.C++绝不是所谓的C的“扩充”——如果C++一开始就起名叫Z语言,你一定不会把C和Z语言联系得那么紧密 117-102 117-301 ;
29.请不要认为学过XX语言再改学C++会有什么问题——你只不过又在学一门全新的语言而已;
30.读完了《Inside The C++
Object Model》以后再来认定自己是不是已经学会了C++;
31.学习编程的秘诀是:编程,编程,再编程;
32.请留意下列书籍:《C++面向对象高效编程(C++ Effective Object-Oriented Software
Construction)》《面向对象软件构造(Object-Oriented Software Construction)》《设计模式(Design
Patterns)》《The Art of Computer Programming》;
33.记住:面向对象技术不只是C++专有的;
34.请把书上的程序例子亲手输入到电脑上实践,即使配套光盘中有源代码;
35.把在书中看到的有意义的例子扩充;
36.请重视C++中的异常处理技术,并将其切实的运用到自己的程序中;
37.经常回顾自己以前写过的程序,并尝试重写,把自己学到的新知识运用进去;
38.不要漏掉书中任何一个练习题——请全部做完并记录下解题思路;
39.C++语言和C++的集成开发环境要同时学习和掌握;
40.既然决定了学C++,就请坚持学下去,因为学习程序设计语言的目的是掌握程序设计技术,而程序设计技术是跨语言的;
41.就让C++语言的各种平台和开发环境去激烈的竞争吧,我们要以学习C++语言本身为主;
42.当你写C++程序写到一半却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成以保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参见43);
43.别心急,设计C++的class确实不容易;自己程序中的class和自己的class设计水平是在不断的编程实践中完善和发展的;
44.决不要因为程序“很小”就不遵循某些你不熟练的规则——好习惯是培养出来的,而不是一次记住的;
45.每学到一个C++难点的时候,尝试着对别人讲解这个知识点并让他理解——你能讲清楚才说明你真的理解了;
46.记录下在和别人交流时发现的自己忽视或不理解的知识点;
47.请不断的对自己写的程序提出更高的要求,哪怕你的程序版本号会变成Version 100.XX;
48.保存好你写过的所有的程序——那是你最好的积累之一;
49.请不要做浮躁的人;
50.请热爱C++!
一.C++语言的基础
说起入门慢,第一个原因莫过于语言基础了.高中时期学校组织的微机兴趣小组学习的是PASCAL语言(我也不知道为什么要讲这个语言,如果说是为了应付比赛,当时也有C语言组呀),所以在大学转向Windows编程的时候,我首先选择了Delphi.大三的时候学校要求考国家二级,二级没有 Delphi,于是又转向了VB,原因是VB做起来与Delphi很像。后来发现VB的IDE做的比Delphi好用,而且BASIC语言写起来简单,于是便弃Delphi而去(明眼人恐怕又要骂我了,若不是太懒,怎么会喜欢VB的IDE呢?的确是这样,后文会提到,懒不仅仅是学习VC的大敌,而且懒人是什么都学不好的)。长年与VB打交道,让我对C/C++语言很不习惯——我不喜欢C++写一个句语要打一个分号,我不喜欢大小写字母要严格区分,我不喜欢比较的时候要写两个等号,我不喜欢……总之,对C++很没好感,没好感也就没兴趣学了(后面提到兴趣是相当重要的)。当然如果你现在再问我应该学习什么语言,我会毫不犹豫地向你推荐C++,因为就常用语言而言,C++语言中包含的知识是相当全面的——从面向过程,到基于对象/面向对象,再到模板和范型,可以说是应有尽有,不夸张地说,别的语言在某种程度上而言是C++语言的子集或者说是在模仿C++、向C++靠拢。
在数次失败中,给我很明显的感觉就是,不学好C++语言就学习Visual
C++纯粹是一种自虐。这次入门之前,我花了3个多月的时间系统地学习了C++语言,够意思吧。然后我信心实足地敲响Visual
C++的家门,呵呵,这次她终于肯给面子了。举个例子吧,在看Dll的调用时,用到“函数指针”,顺理成章就看下去了,想一想如果没有C++语言的基础,基本是不可能的。所以说,没学会中文之前,别看《红楼梦》,那不是《看图识字》。奉劝想从VB转向VC学习的朋友,如果你指望能像学习VB一样边学习VC 边学习C++语言,那你可就错了。
顺便提醒C++语言入门的朋友一点,应该关注ANSI/ISO
C++,也就是标准C++了,市面上C++的书良莠不齐,很多书是“旧书换新皮”,讲的仍然是非标准C++,一定要选好。计算机书很贵,大家不妨找电子版的来看,网上有很多,甚至《C++
Primer》或者《C++沉思录》这样的好书也有热心朋友放到了网上。不过,我最喜欢的是《C++编程金典》这本书,不愧是教育大师写的书,用来学习很合适。至于编译器的选择,如果条件允许就安装VS.NET2003吧,据说Visual
C++7.1的编译器是目前对标准C++支持的最好的编译器了。
二.VC学习资料的选择
VC入门难有很多原因,其中不容忽视的一个就是优秀的VC学习资相对较少。C++语言较深,Visual
C++用起来复杂,再加上资料少——难上加难。资料少,并不意味着没有,怎样选择或者说挖掘就是关键。暂把资料分为光盘、书籍(包括电子书)和文档(包括网上的)三类。
在选择资料方面,大家一定要摈弃中国人思想中的两大劣根性:<1>不劳而获<2>一夜暴富。
“不劳而获”的思想会导致趋向于选择“讲课”类的资料,比如多媒体光盘。结果是光盘容量往往很少但又要求内容面面俱到(不然怎么卖出去呀),这就造成了知识的连贯性差而且讲的又飞快,任你一遍一遍地听,不见成效又打击信心,最后只能放弃。期待早日有内容丰富,讲解精彩的光盘面市。
“一夜暴富”的思想会让你趋向于选择“速成”类教材。那样的教材大多是骗人的——能写个弹出窗口Hello一下World,这就能算是会Visual
C++了?我们还是不要自欺欺人的好。至少也要能连数据库、能使用Socket吧……而这些知识怎么可能“速成”呢?
我有很多Visual
C++的学习资料,但没有一本我是抱着一啃到底的,因为没有哪本书十全十美,我是交替着使用些资料,这样做的好处在于:
<1>知识的连贯性好,跳跃性小,进阶坡度较小,读起来舒服。都说Visual
C++的学习坡度比较陡,那个陡坡是出现在由单纯的C++语言学习转向Windows编程的时候,C++语言本身的学习并没有那么困难。
<2>有积累效应,这本书讲的不精不透,另一本书会帮你补上,这本书你没留心,下本书总该长个心眼。还有就是一些小例子程序,把MFC的类或者函数拆开来给你看,目的非常明确,效果也不错。每天学一点,不图快,图扎实。呵呵,跟VC搞“面向对象”,当然要一天一点恋爱了。
<3>举一返三,动手实践。如果多本书中都把它列为重点,那就一定要熟记在心而且上机操作,书上的例子一定要分析透彻,不能有“差不多”的思想——差多少算多呢?程序这东东,错一个字母都不行呀。光看会了还差远着呢,自己要能写,而且能对例子进行扩展才行。
<4>内容详实丰富,这一点上,首推MSDN啦,还有就是在网上能找到的微软出的Visual
C++的丛书,希望译的电子版,是wdl格式的。虽然MSDN是英文版,但其中的英文并不难——您尽可以相信我,因为在下的英文水平是奇烂无比的。 MSDN有两种用法,一种是当字典用,因为内容全;一种是当消遣,没事了看一个类,敲几行代码,看到那个MFC的继承图了吗,挺好玩儿的,感觉像逛街—— 而且东西不要钱,help
yourself。
互联网上的资源是非常非常丰富的,千万不要错过!好网站和下载站BB皆是。还有论坛、新闻组、在线QQ群……你问我有哪些?呵呵,远在天边近在眼前呀:)
三.内因与外因:“三心二意”和“高手朋友”你有吗?
啊哦,我不是在开玩笑。“三心”是指决心,信心和耐心。决心来源于动机,说来好笑,我最初动机很简单,大学时有个朋友,计算机系的,我总认为我比他聪明(我的天~~~~),他会VC我不会,我就想超过他,现在都毕业两年了,最初的动机早已经不在了,而学习却VC已经成了我的心愿——最关键的一点是我的愿望是写自己的输入法,而写输入法只能用Visual
C++去实现,所以我会有决心学好Visual
C++。至于信心,有两次失败完全是信心不足造成的,促成这次成功的信心说起来还挺传奇:我去北京玩儿,回家的火车上一姓赵位老师看见我别着一个MCP的领章就过来跟我聊天,得知他是一位有着十多年VC开发经验的程序员,敬意油然而生。聊天的过程中,赵老师给了我极大的鼓励和支持——我问他像我这种 Wood
Head能不能在半年内入门VC,他告诉我,一定能,于是我就坚定了自己的信心,现在刚好是4个月,如果赵老师有机会看到这篇文章——我在这里谢谢您啦! (花絮:下车,两个小时后我与女友分手了,是被甩呀同志们!随后的一段日子里,一直与VC相伴……)
还要说说耐心:如果您已经看到这里了,说明您很有耐心(竟然能看到这里还没有拂袖而去),耐心与个人的风格有关,没耐心的人多半是懒人,懒人什么都做不成,学习VC就是不能懒,书懒得看,问题懒得问,英语懒得译……或者是有点挫折就放弃,学好VC是没指望了。我不知道别人怎样,反正我是没少受挫,其实有两次离入门就那么一点点了,我放弃了……学VC要越挫越勇,学VC要肯定执着,Gogogo!
“二意”是指第一你要感觉学习VC有“意思”,二是你要感觉学习VC有“意义”。有意思,就是说你喜欢写程序,“三心”的源动力来源于你对程序设计的热爱,不喜欢编程的人可能能学好VB但绝学不好VC。有意义,就是说你要给自己一个理由:自己都不能给自己一个交待的事情是做不长久的。前面说过,我是为了写自己的输入法,解放中国人的双手,这个理由够纯洁够崇高,还有一个理由就是通过学习VC来砺练自己,成为一个真正的程序员。你可以有自己的理由,比如提高薪水或者取得认证云云,一定要有!这就像是给自己的“报酬”,没有报酬只凭激情做事是任何事都做不长久的。
我小小的成功,有严重的原因是因为我有位“高手朋友”——杨W,他是个VC高手,大家会好奇地问:他教你写什么呢?是MFC还是ATL或者是COM?呵呵,都不是,他从来没教我写过一行代码,但他对我的每一次帮助都弥足珍贵,当我不知道从哪里查找类库资源的时候,他告诉我:MSDN;当我不知道从哪里找到类的成员函数时,他告诉我:在页面的左下角有一个class
member链接,当我问他能不能完成XXXX时,他说:别白费力气了……在他的帮助下,我少走了很多弯路,这也正是高手朋友的可贵之处。在此,我要衷心地说一声:谢谢!
并不是每位学习VC的朋友都有我这么好的运气,如果你身边没有这样的朋友也不用着急吗,我这位好朋友可是经常出没于CSDN的坛坛里,明白了?不过,提醒与我一样的初学者:一定要做一个会问问题的人哦!怎么做一个会问题的人呢?概括一下就是:目的明确,言简意赅,核心代码,客气谦虚。
四.VC入门随笔
本人写东西向来思绪凌乱、颠三倒四。剩下好多东西不知道写到哪里,没办法了,只好叫“随笔”咯。
……学习VC编程,首先要竖立一个“系统/全局观”。无论是VB、C#、Delphi,写程序的时候只需要考虑程序本身就行了,换句话说就是你不用考虑消息是如何映射和传递的。而VC写程序就要多多少少考虑到这些东西。打个比方:以前用VB写程序,就好像是在一座山上建一个亭子,山是山,亭子是亭子,我只管造亭子就是了;而用VC写程序,还是这个亭子,那么你应该意识到,亭子是山的亭子,是山的一部分而不是一个孤立的建筑。“亭子”就是程序,“山”就是Windows系统,亭子的地基是山留给建筑的“接口”,也就是API了……
……VC相对VB入门难,一上来不是像VB那样给个窗体从头做起,而且AppWizard要分好几步,每一步里还有一大堆不知所云的选项,不等生成一个程序就已经晕头转向了。怎么办呢?一句话,从对话框程序入手,因为它最简单,生成的类最少,而且相对是与VB编程最“像”的。在对话框程序里,你可以充分练习添加类和成员变量或者成员函数。……不过我有一点始终搞不明白,由易到难是对话框程序、单文档程序、多文档程序,在AppWizard里微软为什么不按这个顺序排列,非要倒着来呢?成心跟我们这些初学的做对!(国骂省去)……
……又是没大写……又是少分号……又是少一个等号……提醒VB转过来的程序员,别总像我这么没记性哦!
……还是提醒那些学习了VB或者是VB.NET/C#的DDMM,MFC的类虽然是面向对象的,但它没有“属性”这个概念地!不要指望有Me.TextBox1.Text="Hello
World!"这样的语法,C/C++是函数型的语言,类已经把“属性”封装成了成员变量,那些私有的成员变量你看不到,只能通过函数来更改——this ->myTextBox.SetWindowText("Hello
World!");……190-823 117-202 1Z0-043 1z0-042
……晕,原来Win32程序和MFC程序不是一回事呀(看看,这就是一本烂书带给我的,让我一直以为Win32程序就是MFC程序,直到拜读《深入浅出MFC》时才恍然大悟)……
……VC好还是VB好?(拜托,别再问这种无聊的问题了)……
……VC的确能做底层,但不是最底层;VC的确功能强大,但不是万能的——拿手术刀切西瓜或者用菜刀动手术都不对……
……VC高手都是用记事本写程序的:笑不笑由你……190-802 000-834 000-861
……VC程序员比VB程序员强:呵呵,毛主席说过,武器不是战争胜利的决定因素……
……在快速开发工具(RAD)中,控件与后台代码是捆绑在一起的,而MFC的“控件类”不一样,它的“资源”(或者说是皮)与“类”(或者说是瓤)是分开的,要通过ClassWizard把它们“粘”起来……
……如果说C++是一种程序设计语言,那么Visual
C++中的C++语言不如叫“Windows语言”更合适——Visual
C++就是在编程Windows,用到的宏或者Windows数据类型和Windows结构数不胜数,做好心理准备哦!……
……我的天,那么长的函数或者结构都要一个字母一个字母写呀!呵呵,按一下Ctrl+J看看发生了什么?我就奇怪了,几乎没看到有书上提醒我们的初学者要这样去做。这可是着实吓跑了不少初学者呢!(至少我就被吓跑过)。器利工善,我们要把IDE用熟哦,微软送的好礼物可不能浪费……
……很多书在添加完对新话框类之后都写着要在主对话框类里手动添加对这个新类头文件的引用,何必呢?用添加成员变量的方法添加这个新对话框类的实例,头文件自动引用,一举两得。一句话:尽量多用Class
Wizard,能不手写的地方就不手写……
五.virtual BOOL LongWayToGo(void)
{
//头一次写文章,其中Bug肯定少不了,大家一起来DeBug。
//由于是入门级文章,如果有错误,很可能影响初学者学习,恐误人子弟,有错必纠!
//希望大家多提宝贵意见,帮助我前进,谢谢先!
//这是虚函数,留待有所得时续以后文。我还有很长的路……
return TRUE;
}
摘要: 很多人甚至市面上的一些书籍,都使用了void main( ) ,其实这是错误的。C/C++ 中从来没有定义过void main( ) 。C++ 之父 Bjarne Stroustrup 在他的主页上的 FAQ 中明确地写着 The definition void main( ) { /* ... */ } is not and never has been C++, nor has it even been C.( void main( ) 从来就不存在于 C++ 或者 C )。下面我分别说一下 C 和 C++ 标准中对 main 函数的定义
阅读全文
对于崇尚中庸之道的朋友,就不必理会这篇文章了。简单说明一下目前网络安装的简单过程:
安装向导cbuilder2007trialsetup.exe检测是否有.net
2.0环境,这个好办,如果没有安装环境,在网上可以下载到并安装。但是它的本体,全部安装文件400多M,通过安装向导的单线程进行下载,临近每个文件结尾的时候还留下非常充裕的时间给你上wc,你需要上这么多次吗?所以我强烈推荐我们的快车最新版,开8个线程真是牛啊。好了,广告时间已过,开始我们的旅程。
7zip文件管理器,用于解压7zip格式的文件:
http://www.skyuc.com/bbs/showthread.php?t=62&goto=nextoldest
.net 2.0环境所需要安装的3个文件:
1)http://installers.codegear.com/prereq/radstudio/5.0/microsoft%20.net%20framework%202.0.7zip
2)http://installers.codegear.com.edgesuite.net/prereq/radstudio/5.0/microsoft%20jsharp%20runtime%202.0.7zip
3)http://installers.codegear.com.edgesuite.net/prereq/radstudio/5.0/microsoft%20.net%202.0%20english%20framework%20sdk%20x86.7zip
或
http://www.microsoft.com/downloads/info.aspx?na=90&p=&SrcDisplayLang=zh-cn&SrcCategoryId=&SrcFamilyId=fe6f2099-b7b4-4f47-a244-c96d69c35dec&u=http%3a%2f%2fdownload.microsoft.com%2fdownload%2fb%2fe%2fa%2fbea35549-7804-4e28-beef-a7d9d1675f4c%2fsetup.exe
在www.codegear.com上注册,拿到你的安装序列号,下载安装向导cbuilder2007trialsetup.exe:
C++Builder 2007 Enterprise Trial
Serial Number: xxxxx
CDN Login
Name: xxx
First Name: xxx
Last Name: xxx
重要提示:注意下文的编号,如1)和(1)是不同的,并不是我写错了
需要特别注意的4个目录:
1)2007安装目录,自定义:我的为J:\CodeGear\RAD Studio\5.0
2)示例文件目录,自定义:J:\CodeGear\RAD Studio\5.0\Demos
3)安装向导存储2007安装文件用到的临时目录,自定义:H:\download\bcb2007
4)安装向导cbuilder2007trialsetup.exe解压3)的安装文件用到的临时目录,不可自定义,所以一般要保证c盘剩余空间在1.2g以上(安装了.net
2.0之后,系统仍提示需要1.7g,实际上不要这么多,可以忽视它的提示)对于所有用户使用方式安装的为:C:\Documents and Settings\All
Users\ApplicationData\{2EB4C530-C94F-4893-ABDC-C1E05A89956E}
安装的4个步骤:
(1)安装.net 2.0环境的3个文件,网上遍地都是。如果是7zip格式的文件,你要先安装7zip的解压软件
(2)运行cbuilder2007trialsetup.exe,设置各种目录,直到它开始向3)的目录下载并自动解压到4)中,这是一个环绕地球80天的等待步骤,就让它自动运行好了
(3)在(2)的同时,我们用快车之类的软件先下载3)用到的所有文件约423M并放到3)中的目录,下面的链接你可以手工一条条地输入快车,也可以做成一个html文件批量导入,方法我就不说了,留给大家发挥:
http://installers.codegear.com/release/radstudio/11.0.2709.7128/vcldotnetruntimes.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/vclwin32runtimes.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bcbwin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dunit.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/core.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/core%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corex.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corecx.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/transm
// 本文转自 C++Builder 研究 -
http://www.ccrun.com/article.asp?i=1024&d=lmp27sogrifierc.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corewin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corewin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/xmlmapper.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/coredelphibcb.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/database.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/databasew32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexplr.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ibxw32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ibxbcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ibxbcbwin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbgow32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sqlbuilder.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sqlbuilder%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbxcomponentsw32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexpress.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexpress%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/dbexpressx.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/unittesting.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/unittesting_bcb.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/htmldesigner.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/htmldesigner%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/ite.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/etm.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/itewin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/indywin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/indybcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/intraweb.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/iwbcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sampleprograms.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/samplesbcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpwin32%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpcommon.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/helpcommon%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualization.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualizationx.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualizationx%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codevisualization_bcbw32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/internetcontrols.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/internetctrlswin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/internetctrlswin32x.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/soapwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/webappdebugger.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/msofficecontrolscore.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/msofficebcbwin32.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/corba.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/codeguard.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bde.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bde_pro.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon_pro.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/bdecommon_pro%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/databasedesktop.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/databasedesktop%20english.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/imagefiles.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/sampledatafiles.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/rave%20reports%20installer.7zip
http://installers.codegear.com/release/radstudio/11.0.2709.7128/borland%20database%20engine%20professional%20english.7zip
(4)完成了(3)之后,停止(2)的运行,再重新运行(2),这时候它就不再下载,只会自动解压3)中的所有文件并放到4)去。当然,(3)中的文件下漏几个也无所谓,反正它会补下。它仍需要通过网络验证这些文件的有效性,所以这也是一个环游地球一圈的漫长时间。最后,提示你注册,如果你没有那个可爱的破解文件bds.exe(版本11.0.2709.7128),估计你会进不去。如果你今天才来,很不幸,http://www.ccrun.com/forum/forum_posts.asp?TID=6071提到的破解文件已经被删除了,但是我成为了幸运者,所以我才能够完成这篇文章。替换bds.exe,运行,启动速度比起我无法忍受的turbo
c++ 2006 professional(和bds 2006是兄弟)快了很多。com支持已经存在了,非常好。
TTreeView是VCL中提供的树列表控件,树的每个节点是一个TTreeNode类,TTreeNode组件的属性和方法可以参考Borland提供的帮助(虽然不如MSDN全面,但有总比没有强)。实际应用中我们可能需要禁用某个节点(界面上反应的效果是:节点字体呈灰色显示,节点无法选中等)。但是VCL没有提供Node->Disable();或Node->Enable = false;这样的功能,我们只好自己动手实现了。首先我们需要为每个节点设定一个标志,用来标识此节点是否可用,标识方法有很多,比如判断节点的文本(Text),节点的绝对索引值(AbsoluteIndex),节点的索引(Index)加缩进(Indent)等,在本例中我们用节点的Data属性作标识(一个void
*型数据,其实可以存放N多东西)。如果在你的应用中恰好用了Data属性,就另外想个用来作标志的方法吧。:)
我们写一个自定义函数,用来启用/禁用一个节点:
void __fastcall CrnEnableTreeNode(
bool bEnable, TTreeNode *pNode)
{
pNode->Data = bEnable? NULL: (
void *)0xFFFF;
// 本文转自 C++Builder 研究 -
http://www.ccrun.com/article.asp?i=1015&d=r2tf61
pNode->TreeView->Invalidate();
}
然后考虑如何达到禁用节点的效果,前面说了,我们只需实现这两个效果:
1. 节点字体呈灰色显示
2. 节点无法选中
节点字体呈灰色显示可以通过TreeView的OnCustomDrawItem事件中的自绘实现,在设计时状态,选中TreeView,Events选项卡双击OnCustomDrawItem事件,添加以下代码:
void __fastcall TForm1::TreeView1CustomDrawItem(TCustomTreeView *Sender,
TTreeNode *Node, TCustomDrawState State,
bool &DefaultDraw)
{
if(
int(Node->Data) == 0xFFFF)
{
Sender->Canvas->Font->Color = clGray;
}
}
节点无法选中则可以通过TreeView的Changing事件来处理,在设计时状态,选中TreeView,Events选项卡双击OnChanging事件,添加以下代码:
void __fastcall TForm1::TreeView1Changing(TObject *Sender, TTreeNode *Node,
bool &AllowChange)
{
AllowChange = (
int(Node->Data) != 0xFFFF);
}
有以上的实现,效果基本就出来了:
测试代码:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if(TreeView1->Selected)
CrnEnableTreeNode(
true, TreeView1->Selected);
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
CrnEnableTreeNode(
true, TreeView1->Items->Item[1]);
}
为看到比较好的效果,可在测试时展开所有节点为:
TreeView1->FullExpand();