c++的一些编程技巧和细节

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++的一些编程技巧和细节相关的知识,希望对你有一定的参考价值。

1、有时候,函数参数多的时候,最好也换行,如:

    CreateProcess(
                  NULL,
                  cmdbuf,
                  NULL,
                  NULL,
                  bInhH,
                  dwCrtFlags,
                  envbuf,
                  NULL,
                  &siStartInfo,
                  &prInfo
                 );

函数的参数个数最好不要太多,一般来说6个左右就可以了,众多的函数参数会让读代码的人一眼看上去就很头昏,而且也不利于维护。如果参数众多,还请使用结构来传递参数。这样做有利于数据的封装和程序的简洁性。

也利于使用函数的人,因为如果你的函数个数很多,比如12个,调用者很容易搞错参数的顺序和个数,而使用结构struct来传递参数,就可以不管参数的顺序。

而且,函数很容易被修改,如果需要给函数增加参数,不需要更改函数接口,只需更改结构体和函数内部处理,而对于调用函数的程序来说,这个动作是透明的。

写有参数的函数时,首要工作,就是要对传进来的所有参数进行合法性检查。而对于传出的参数也应该进行检查,这个动作当然应该在函数的外部,也就是说,调用完一个函数后,应该对其传出的值进行检查。

例如输入参数,如果是指针要先判断是否为空或非法

对于函数返回值

如果是系统函数,则需要判断返回值做出相应判断,而不是默认他为正常的返回

如果是返回布尔值,则应先判断布尔值的真假

如果是指针,则更应检查指针的合法性

 

2、对于选择语句的使用

如果是正常和出错的选择,一般先把出错选项放在前面,因为出错选项的分支比较小,也可以让人清晰的知道各种可能出错的选项

如果是对于常用的和不常用的选项,一般将常用的选项放在前面,这样可以减少程序执行时选择的时间

 

3、对于错误提示的处理

一种是具体情况具体分析,在每次错误提示的地方定义错误提示的代码,这种方法虽然比较灵活,但是也比较不规范,第一是每次都要在出错的地方重复一遍错误提示的代码,另一个是同样错误类型可能会有不太一致的错误提示。

另外一种就是在头文件中通过枚举或const定义所有可能的错误类型,每个变量名用错误描述定义,同时定义一个处理错误函数,函数的形参是错误类型变量,在函数内定义一个选择语句,每一个分支都对应与一个错误的类型。

 

4、修改别人的代码

当你维护别人的程序时,请不要非常主观臆断的把已有的程序删除或是修改。我经常看到有的程序员直接在别人的程序上修改表达式或是语句。修改别人的程序时,请不要删除别人的程序,如果你觉得别人的程序有所不妥,请注释掉,然后添加自己的处理程序,必竟,你不可能100%的知道别人的意图,所以为了可以恢复,请不依赖于CVS或是SourceSafe这种版本控制软件,还是要在源码上给别人看到你修改程序的意图和步骤。

 

5、宏

虽然,宏的执行很快(因为没有函数调用的开销),但宏会让源代码澎涨,使目标文件尺寸变大,(如:一个50行的宏,程序中有1000个地方用到,宏展开后会很不得了),相反不能让程序执行得更快(因为执行文件变大,运行时系统换页频繁)。

 

6、static的变量

一个static的变量,其实就是全局变量,只不过他是有作用域的全局变量。比如一个函数中的static变量static的最多的用处却不在这里,其最大的作用的控制访问,在C中如果一个函数或是一个全局变量被声明为static,那么,这个函数和这个全局变量,将只能在这个C文件中被访问,如果别的C文件中调用这个C文件中的函数,或是使用其中的全局(用extern关键字),将会发生链接时错误。这个特性可以用于数据和程序保密。

 

7、函数长度

函数一般是完成一个特定的功能,千万忌讳在一个函数中做许多件不同的事。函数的功能越单一越好,一方面有利于函数的易读性,另一方面更有利于代码的维护和重用,功能越单一表示这个函数就越可能给更多的程序提供服务,也就是说共性就越多。

一般来说,一个函数中的代码最好不要超过300行左右,越少越好,最好的函数一般在100行以内。有证据表明,一个函数中的代码如果超过300行,就会有和别的函数相同或是相近的代码,也就是说,就可以再写另一个函数。

虽然函数的调用会有一定的开销,但比起软件后期维护来说,增加一些运行时的开销而换来更好的可维护性和代码重用性,是很值得的一件事。

 

8、硬编码

最好不要在程序中出现数字式的硬编码例如在数组大小或者循环次数时,尽量不要直接使用数字,而是定义常量来使用,这样在使用的时候就知道了这个值是干什么用的

 

9、调试信息

程序在开发过程中必然有许多程序员加的调试信息。我见过许多项目组,当程序开发结束时,发动群众删除程序中的调试信息,何必呢?为什么不像VC++那样建立两个版本的目标代码?一个是debug版本的,一个是Release版的。那些调试信息是那么的宝贵,在日后的维护过程中也是很宝贵的东西,怎么能说删除就删除呢?

 

10、字符串常量

对于像

char *p = “test”;

*p = ‘p’;

这样的赋值操作,虽然可以正常编译,但是赋值语句却并不起作用,因为test”是常量,是不能再被赋值的,编译器会自动把它定义为常量指针,而在运行时,在vs2008环境下,如果是release模式不会报错,在debug模式下运行到赋值语句时,程序会直接崩溃。并且如果是两个指针指向相同的字符串常量,这两个指针指向的是同一地址的字符串。因此要避免这种定义,如果必须定义一个这样的常量可以用如下的格式:

const char *p = “test”;

这样就显式声明了p是一个常量指针,若给它赋值编译器就会报错。

若想用指针指向一个一般的字符串,可以用下面的方法

char *p = new char[5];

//p = "test";

if (p)

{

  strcpy(p, "test");

  p[0] = _T(‘4‘);

  AfxMessageBox(p);

  delete[] p;

  p = NULL;

}

上面的赋值操作都是正常的

但是如果加上上面那句被注释的语句,就会出现问题。因为该条语句相当于让p指向了“test”常量的字符串,而不再指向new分配的地址,这样下面的赋值语句不起作用,因为它现在是一个指针常量了,使用delete也不在起作用,因为p现在没有指向new分配的地址。并且造成了内存泄漏。

或者

char a[5] = {0};

strcpy(a, "test");

AfxMessageBox(a);

a[0] = ‘0‘;

AfxMessageBox(a);

当然使用stringCStirng类也可以避免上面的问题。

 

11、对于引用

将引用初始化之后,引用就一直指向这个变量,相当于指针常量,再用别的变量赋值,也是将这个变量的值赋给乐引用初始化对应的变量。

 

12、字符串函数

strcmp,strlen,strcpy,strcat这些字符串函数不能直接操作空指针,否则就会直接出现内存错误,因此在操作之前一定要判断指针是否为空。

 

13、定义与初始化

初始化式必须要有存储空间来进行初始化,如果声明有初始化式,那么它可被当做是定义,即使声明标记为extern

头文件中只能存放变量,函数的声明和内联函数的定义,变量默认为extern型,但要有事先声明,方法是在cpp文件中定义变量,在其对应头文件中用extern声明文件,然后在用到该变量的cpp文件中#include包含该变量声明的头文件即可,当然若不想该变量被其他文件访问,可在定义时在其前面加static前缀,这样即使在头文件中用extern声明,它也不能被外部文件访问

 

14、指针和引用的区别

1)、 引用必须初始化,不能为空,指针可以为空;引用被赋给一个变量后就不能再被赋值,指针可以随意更改,所以没有const引用,指针却有。指针是解引用,引用不是。指针可以嵌套使用,引用不行。

2)、 sizeof时,指针得到的是指针的大小,引用是变量的大小

3)、 指针和引用的加减操作不同

 

15、赋值构造函数和复制构造函数的区别

1)、 拷贝是创建新的对象并对其赋值,赋值是对已经存在的对象进行赋值。

2)、 当对象内有堆分配指针时,注意深拷贝和浅拷贝的区别。当为复制时需要先为新对象分配空间,然后再将要赋的值赋给它,如果只传递指针值,就会造成多个指针指向同一块内存空间,造成重复释放出现问题。如果是赋值,就要先判断是不是同一对象,是的话就直接返回,如果不是,就先释放掉原来的内存,然后分配新的内存并将值写到新的内存中。

3)、 拷贝函数的形参是引用,无返回值,赋值函数的形参和返回值都是引用。因为拷贝函数是构造函数,构造函数是没有返回值的,使用引用而不是值传递可以避免重复调用拷贝构造函数,生成很多副本,最重要的是使用值传递就会造成拷贝函数的无限循环递归调用。

综上所述,需要自定义拷贝构造函数和赋值函数的情形之一就是类中存在动态分配内存的变量。如果赋值函数使用默认函数,就会造成原来的内存地址泄露,同时有多个指针指向同一块内存,造成重复释放。使用默认拷贝函数的问题只会将指针值传递过去,造成个指针指向同一块内存,造成重复释放。

 

16、虚函数表

没有虚函数的类没有虚函数表

在类对象分配内存时,首先分配存储该类虚函数表地址的指针空间,并且一个类在内存中只有一份虚函数表,该类的所有对象调用的都是这一份虚函数表

vs2008中,虚函数表的首地址并不就是第一个虚函数的地址,并且该表中只存储是虚函数的地址,不是虚函数的地址不存储,因此不同版本的编译器其定义可能不太一样

 

17、C++中的explicit

C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数)有两个作用:第一就是一个形参的构造函数,第二就是隐式的赋值操作函数,但有时想要不允许这样的隐式转换,则可在构造函数前面加关键字explicit

 

18、基类和派生类的类型兼容规则

指向派生类的基类的指针不能直接访问派生类中新增的成员。

需要知道一些常识,一个类所有的函数都是在code代码区中唯一的存放一份。而数据成员则是每个对象存储一份,并按照声明顺序依次存放。
    A中有了虚函数就会在类的数据成员的最前面添加一个vfptr指针(void** vfptr),这个指针用来指向一个vtable表(一个函数指针数组)(一个类只有一个该表),该表存储着当前类的所有虚函数的地址。这样vfptr就成为了一个类似成员变量的存在。访问虚函数的时候通过vfptr间址找到vtable表,再间址进而找到要调用的函数。这样就在一定程度上摆脱了类型制约。
    只要vfptr的值不同,那么访问函数成员的时候使用的vtable表就不同,就可能访问到不同类的函数成员。B类对象中的vptr指向B类自己的vtable。
    B类继承A类的时候,因为A中有虚函数,编译器就自动的给B类添加vfprt指针和vtable表。也可以理解为B类继承来了A类中的那个vptr指针成员。
    A类指针指向B类对象时,发生假切割。要知道这个过程只是切掉A类中没有的那些成员,由于vfptr是从A类中继承来的,所以这个量仍将保留。而对于vfptr的值则不会改变,仍然指向B类的vtable表。所以访问F1函数的时候是通过B类的vtable表去寻址的,自然就是使用子类的函数。
    B类的指针指向A类的对象时(当B类存在新增数据成员时可能出错),同理。
    而对于普通函数则受类型的制约,(因为没有vptr指针)使用哪个类的指针调用函数,那么所调用的就是那个类的函数。
    总而言之,普通函数通过对象或指针的类型来找所调用的函数,而虚函数是通过一个指针来找到所要调用的函数的。

当将一个派生类对象赋给基类指针时,该基类指针不能访问派生类中新增的成员,但将基类指针强制转化为派生类指针后则可以访问派生类中新增的成员。

 

19、四种类型转换

reinterpret_cast

进行二进制形式的转化,一般不用

const_cast

用于取出const属性,把const类型的指针变为非const类型的指针,如:const int *fun(int x,int y){}  int *ptr=const_cast<int *>(fun(2.3))

static_cast

static_cast其实就是隐式转化与普通显示强制转化,不会进行类型安全检查,这样在转换之后调用就会出问题,例如,将基类对象赋给继承类指针,用该指针去访问继承类的新增成员就会出现错误,但在转化的时候不会出错误,在调用的时候才会出错,有很大的隐患。C语言的类型转换其实就是这种转换。

dynamic_cast

利用dynamic_cast则会先进行类型检查,如果不行就直接报错,也不会去进行转化,pd1 = dynamic_cast<D*>(pb),如果成功则pd1指向pb,如果失败则pd1为空,可以将语句放在条件语句的条件执行中,如果失败则该表达式为0,就可以得到是执行失败了,进行失败的处理。

但要注意的是,使用dynamic_cast的类中要有虚函数,否则就会报错,这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。

对于引用,因为不存在空引用,所以不可能对引用使用用于指针强制类型转换的检查策略,相反,当转换失败的时候,它抛出一个 std::bad_cast 异常,该异常在库头文件 typeinfo 中定义。可以重写前面的例子如下,以便使用引用:

void f(const Base &b)

{

try

{

const Derived &d = dynamic_cast<const Derived&>(b);

// use the Derived object to which b referred

}

catch (bad_cast)

{

// handle the fact that the cast failed

}

}

 

20、数组做函数形参

void Func ( char str[100] )
{
 sizeof( str );//结果是4
}


  Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。


数组名的含义如下:
1)数组名指代一种数据结构,这种数据结构就是数组;
  例如:
char str[10];
cout << sizeof(str) << endl;
输出结果为10str指代数据结构char[10]


2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改;
char str[10];
str++; //编译出错,提示str不是左值

 
3)数组名作为函数形参时,沦为普通指针。

































以上是关于c++的一些编程技巧和细节的主要内容,如果未能解决你的问题,请参考以下文章

C++中注意不到的细节

C++中注意不到的细节

C++编程题

细节问题

C++编程法则100条关于内嵌类的使用细节

服务器端编程心得—— 关于网络编程的一些实用技巧和细节