以文本方式查看主题

-  计算机科学论坛  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  VC编程中需注意的几个问题  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=27057)


--  作者:卷积内核
--  发布时间:2/8/2006 8:49:00 AM

--  VC编程中需注意的几个问题
const要注意的问题
  1、下面是一个几乎所有人刚开始都会搞错的问题:
已知:typedef char *cstring;
在以下声明中,cstr的类型是什么?
extern const cstring cstr;

错误答案:const char *cstr;
正确答案:char *const cstr;

  错误在于将typedef当作宏扩展。const 修饰cstr的类型。cstr是一个指针,因此,这个定义声明了cstr是一个指向字符的const指针。
  2、指针是const还是data为const?
辨别方法很简单,如下:

代码:
char *p="hello"; //non-const pointer, non-const data;
const char *p="hello"; // non-const pointer, const data;
char * const p="hello"; // const pointer , non-const data;
const char * const p="hello"; // const pointer, const data;


  要注意的是,"hello"的类型是const char * ,按C++standard规则,char *p="hello" 是非法的(右式的const char* 不能转换为左式的char *),违反了常量性。但是这种行为在C中实在太频繁,因此C++standard对于这种初始化动作给予豁免。尽管如此,还是尽量避免这种用法。
  3、const初始化的一些问题
const 对象必须被初始化:

代码:
const int *pi=new int; // 错误,没有初始化
const int *pi=new int(100); //正确
const int *pci=new const int[100]; //编译错误,无法初始化用new表达式创建的内置类型数组元素。

什么时候需要copy constructor,copy assignment operator,destructor
  注意,若class需要三者之一,那么它往往需要三者。
当class的copy constructor内分配有一块指向hcap的内存,需要由destructor释放,那么它也往往需要三者。

为什么需要protected 访问级别
  有人认为,protected访问级别允许派生类直接访问基类成员,这破坏了封装的概念,因此所有基类的实现细节都应该是private的;另外一些人认为,如果派生类不能直接访问基类的成员,那么派生类的实现将无法有足够的效率供用户使用,如果没有protected,类的设计者将被迫把基类成员设置为public。
  事实上,protected正是在高纯度的封装与效率之间做出的一个良好折衷方案。

为什么需要virtual member function又不能滥用virtual
  若基类设计者把本应设计成virtual的成员函数设计成非virtual,则继承类将无法实现改写(overridden),给继承类的实现带来不便;
  另一方面,一旦成员函数被设计成virtual,则该类的对象将额外增加虚拟指针(vptr)和虚拟表格(vtbl),所以倘若出于方便继承类overridden的目的而使所有成员函数都为virtual,可能会影响效率,因为每个virtual成员函数都需付出动态分派的成本。而且virtual成员函数不能内联(inline),我们知道,内联发生在编译时刻,而虚拟函数在运行时刻才处理。对于那些小巧而被频繁调用、与类型无关的函数,显然不应该被设置成virtual。

关于引用的一些注意点
  1、把函数参数声明为数组的引用:当函数参数是一个数组类型的引用时,数组长度成为参数和实参类型的一部分,编译器检查数组实参的长度和与在函数参数类型中指定的长度是否匹配。

代码:
//参数为10个int数组
void showarr(int (&arr)[10]);
void func()
{
  int i,j[2],k[10];
  showarr(i); //错误!实参必须是10个int的数组
  showarr(j); //错误!实参必须是10个int的数组
  showarr(k); //正确!
}

//更灵活的实现,借助函数模板。下面是一个显示数组内容的函数。
template <typename Type , int size>
void printarr(const Type (& r_array)[size])
{
 for(int i=0;i<size;i++) std::cout<< r_array[i] <<' ';
 std::cout << std::endl;
}

void caller()
{
   int ar[5]={1,2,5,3,4}; //数组可以任意大小。
   printarr(ar); //正确!自动正确调用printarr()
}


goto语句的一些要注意的地方
  1、label语句只能用作goto的目标,且label语句只能用冒号结束,且label语句后面不能紧接右花括号'}',如

代码:
label8: }


  办法是在冒号后面加一个空语句(一个';'即可),如

代码:
label7: ;}


   2、goto语句不能向前跳过如下声明语句:

代码:
goto label6;
int x=1; //错误,不能跳过该声明!
cout<<x<<endl; //使用x
label6:
//其他语句


但是,把int x=1; 改为int x; 则正确了。另外一种方法是:

代码:
goto label6;
{
  int x=1; //正确,使用了语句快
  cout<<x<<endl;
}
label6:
//其他语句


  3、goto语句可以向后(向程序开头的方向)跳过声明定义语句。

代码:
begin:
int i=22;
cout<< i <<endl;
goto begin; //非常蹩脚,但它是正确的

变量作用域
  1、花括号可以用来指明局部作用域。
  2、在for、if、switch、while语句的条件/循环条件中可以声明变量,该变量仅在相应语句块内有效。
  3、extern为声明但不定义一个对象提供了一种方法;它类似于函数声明,指明该对象会在其他地方被定义:或者在此文本的其他地方,或者在程序的其他文本文件中。例如extern int i; 表示在其他地方存在声明 int i;
  extern 声明不会引起内存分配,他可以在同一个文件或同一个程序中出现多次。因此在全局作用域中,以下语句是正确的:

代码:
extern int c;
int c=1;  //没错
extern int c; //没错


  但是,extern声明若指定了一个显式初始值的全局对象,将被视为对该对象的定义,编译器将为其分配存储区;对该对象的后续定义将出错。如下:

代码:
extern int i=1;
int i=2; //出错!重复定义

auto_ptr若干注意点
  1、auto_ptr的主要目的是支持普通指针类型相同的语法,并为auto_ptr所指对象的释放提供自动管理,而且auto_ptr的安全性几乎不会带来额外的代价(因为其操作支持都是内联的)。定义形式有三种:

代码:
auto_ptr<type_pointed_to>identifier(ptr_allocated_by_new);
auto_ptr<type_pointed_to>identifier(auto_ptr_of_same_type);
auto_ptr<type_pointed_to>identifier;


  2、所有权概念。auto_ptr_p1=auto_ptr_p2的后果是,auto_ptr_p2丧失了其原指向对象的所有权,并且auto_ptr_p2.get()==0。不要让两个auto_ptr对象拥有空闲存储区内同一对象的所有权。注意以下两种种初始化方式的区别:

代码:
auto_ptr<string>auto_ptr_str1(auto_ptr_str2.get()); //注意!用str2指针初始化str1, 两者同时拥有所有权,后果未定义。
auto_ptr<string>auto_ptr_str1(auto_ptr_str2.release());//OK!str2释放了所有权。

  
3、不能用一个指向“内存不是通过应用new表达式分配的”指针来初始化或者赋值auto_ptr。如果这样做了,delete表达式会被应用在不是动态分配的指针上,这将导致未定义的程序行为。

C风格字符串结尾空字符问题

代码:
char *str="hello world!"; //str末尾自动加上一个结尾空字符,但strlen不计该空字符。
char *str2=new char[strlen(str)+1] // +1用来存放结尾空字符。


定位new表达式
  头文件:<new>
  形式:new (place_address) type-specifier
  该语句可以允许程序员将对象创建在已经分配好的内存中,允许程序员预分配大量的内存供以后通过这种形式的new表达式创建对象。其中place_address必须是一个指针。例如:

代码:
char *buf=new char[sizeof(myclass-type)*16];
myclass-type *pb=new (buf) myclass-type; //使用预分配空间来创建对象
// ...
delete [] buf;  // 无须 delete pb。


名字空间namespace

  1、namespace的定义可以是不连续的(即namespace的定义是可以积累的),即,同一个namespace可以在不同的文件中定义,分散在不同文件中的同一个namespace中的内容彼此可见。这对生成一个库很有帮助,可以使我们更容易将库的源代码组织成接口和实现部分。如:在头文件(.h文件)的名字空间部分定义库接口;在实现文件(如.c或.cpp文件)的名字空间部分定义库实现。名字空间定义可积累的特性是“向用户隐藏实现细节”必需的,它允许把不同的实现文件(如.c或.cpp文件)编译链接到一个程序中,而不会有编译错误和链接错误。
  2、全局名字空间成员,可以用“::member_name”的方式引用。当全局名字空间的成员被嵌套的局部域中声明的名字隐藏时,就可以采用这种方法引用全局名字空间成员。
  3、名字空间成员可以被定义在名字空间之外。但是,只有包围该成员声明的名字空间(也就是该成员声明所在的名字空间及其外围名字空间)才可以包含它的定义。
  尤其要注意的是#include语句的次序。假定名字空间成员mynamespace::member_i的声明在文件dec.h中,且#include "dec.h"语句置于全局名字空间,那么在include语句之后定义的其他名字空间内,mynamespace::member_i的声明均可见。即,mynamespace::member_i可以在#include "dec.h"之后的任何地方任何名字空间内定义。
  4、未命名的名字空间。我们可以用未命名的名字空间声明一个局部于某一文件的实体。未命名的名字空间可以namespace开头,其后不需名字,而用一对花括号包含名字空间声明块。如:

代码:
// 其他代码略
namespace
{
 void mesg()
 {
  cout<<"**********\n";
 }
}
int main()
{
 mesg(); //正确
 
    //...

 return 0;
}


  由于未命名名字空间的成员是程序实体,所以mesg()可以在程序整个执行期间被调用。但是,未命名名字空间成员只在特定的文件中可见,在构成程序的其他文件中是不可以见的。未命名名字空间的成员与被声明为static的全局实体具有类似的特性。在C中,被声明为static的全局实体在声明它的文件之外是不可见的。

using关键字
  1、using声明与using指示符:前者是声明某名字空间内的一个成员,后者是使用整个名字空间。例如:

代码:
using cpp_primer::matrix; // ok,using声明
using namespace cpp_primer; //ok,using指示符


  2、 该using指示符语句可以加在程序文件的几乎任何地方,包括文件开头(#include语句之前)、函数内部。不过用using指定的名字空间作用域(生命周期)受using语句所在位置的生命周期约束。如,函数内部使用“using namespace myspacename;”则 myspacename仅在该函数内部可见。
  3、可以用using语句指定多个名字空间,使得多个名字空间同时可见。但这增加了名字污染的可能性,而且只有在使用各名字空间相同成员时由多个using指示符引起的二义性错误才能被检测到,这将给程序的检测、扩展、移植带来很大的隐患。因此,因该尽量使用using声明而不是滥用using指示符。

重载函数
  1、如果两个函数的参数表中参数的个数或者类型不同,则认为这两个函数是重载的。
  如果两个函数的返回类型和参数表精确匹配,则第二个声明被视为第一个的重复声明,与参数名无关。如 void print(string& str)与void print(string&)是一样的。
  如果两个函数的参数表相同,但是返回类型不同,则第二个声明被视为第一个的错误重复声明,会标记为编译错误。
  如果在两个函数的参数表中,只有缺省实参不同,则第二个声明被视为第一个的重复声明。如int max(int *ia,int sz)与int max(int *, int=10)。
  参数名类型如果是由typedef提供的,并不算作新类型,而应该当作typedef的原类型。
  当参数类型是const或者volatile时,分两种情况:对于实参按值传递时,const、volatile修饰符可以忽略;对于把const、volatile应用在指针或者引用参数指向的类型时,const、volatile修饰符对于重载函数的声明是有作用的。例如:

代码:
//OK,以下两个声明其实一样
void func(int i);
void func(const int i);

//Error,无法通过编译,因为func函数被定义了两次。
void func(int i){}
void func(const int i){}

//OK,声明了不同的函数
void func2(int *);
void func2(const int *);

//OK,声明了不同的函数
void func3(int&);
void func3(const int&);


  2、链接指示符extern "C"只能指定重载函数集中的一个函数。原因与内部名编码有关,在大多数编译器内部,每个函数明及其相关参数表都被作为一个惟一的内部名编码,一般的做法是把参数的个数和类型都进行编码,然后将其附在函数名后面。但是这种编码不使用于用链接指示符extern "C"声明的函数,这就是为什么在重载函数集合中只有一个函数可以被声明为extern "C"的原因,具有不同的参数表的两个extern "C"的函数会被链接编辑器视为同一函数。例如,包含以下两个声明的程序是非法的。

代码:
//error:一个重载函数集中有两个extern "C"函数
extern "C" void print(const char*);
extern "C" void print(int);

函数模板
  1、定义函数模板:

代码:
template <typename/class identifier, ...>
[inline/extern]
ReturnType FunctionName(FuncParameters...)
{
  //definition of a funciton template...
}


W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
46.875ms