c++-容易被忽略的编程细节

罗列了一些容易被忽略的C++编程细节,在阅读ROS源代码时时常会遇到,理解它们的作用内涵,有助于对源代码的深入理解。

1.变量的隐式初始化形式

一般来说,声明一个变量,然后显示地对其初始化,形式如下:

int ival = 1024;
string project = "Fantasia 2000";

下面是不太常用的变量初始化的隐式形式,变量的初始值被放在括号中:

int ival( 1024 );
string project( "Fantasia 2001" )

该方法产生的效果与上面显示的赋值是一样的。 另外,每种内置数据类型都支持一种特殊的构造函数语法,可将对象初始化为0。 例如

// 设置 ival 为 0 dval 为 0.0
int ival = int();
double dval = double();

2.静态局部变量(static)

声明变量的时候,在变量前增加static关键字,即声明了静态局部变量。 静态局部变量属于静态存储方式,它具有以下特点: (1)静态局部变量在函数内定义,但不像自动变量那样,当调用时就存在,退出函数时就消失。静态局部变量始终存在着,也就是说它的生存期为整个源程序。 (2)静态局部变量的生存期虽然为整个源程序,但是其作用域仍与自动变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。 (3)允许对构造类静态局部量赋初值。若未赋以初值,则由系统自动赋值。数值型变量自动赋初值0,字符型变量赋空字符。 (4)对基本类型的静态局部变量若在说明时未赋以初值,则系统自动赋予0值。而对自动变量不赋初值,则其值是不定的。 根据静态局部变量的特点, 可以看出它是一种生存期为整个源文件的量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可继续使用, 而且保存了前次被调用后留下的值。 因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用局部静态变量为宜。 给读者一个简单直白的例子(区别静态局部变量和动态局部变量):

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int fun(int n)
{
static int f=1;
f=f*n;
return f;
}
void main()
{
int i;
for(i=1;i<=5;i++)
printf("fun(%d)=%d\n",i,fun(i));
}

这里的运行结果是:

1
2
3
4
5
fun(1)=1
fun(2)=2
fun(3)=6
fun(4)=24
fun(5)=120

说明f在加了static的类型限制之后,就相当于全局变量,函数调用完了之后,修改过的f的值仍然是有效的(即这个程序相当于求i的阶乘了)。而如果不加static的类型限制,那么,会是什么结果呢,我们看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int fun(int n)
{
int f=1;
f = f * n;
return f;
}
void main()
{
int i;
for(i=1;i<=5;i++)
printf("fun(%d)=%d\n",i,fun(i));
}

程序的运行结果是:

1
2
3
4
5
fun(1)=1
fun(2)=2
fun(3)=3
fun(4)=4
fun(5)=5

也就是说,这时函数fun中的变量f的生命周期就仅限于fun函数的范围内了,在main中每次传入新的参数i,f就会计算1*i的值并返回,而不会像之前那样不断的累乘了。

3.const常量

const修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的。 const关键字的作用主要有以下几点: (1)可以定义const常量,具有不可变性。 例如:

1
const int Max=100; int Array[Max];

(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。例如: void f(const int i) { ………} 编译器就会知道i是一个常量,不允许修改; (3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 (4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。 还是上面的例子,如果在函数体内修改了i,编译器就会报错; 例如:

1
void f(const int i) { i=10;//error! }

(5) 为函数重载提供了一个参考。 class A { …… void f(int i) {……} //一个函数 void f(int i) const {……} //上一个函数的重载 …… }; (6) 可以节省空间,避免不必要的内存分配。 例如:

1
2
3
4
5
6
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。 (7) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

4.内联函数inline

内联函数是指用inline关键字修饰的函数。在类内定义的函数被默认成内联函数。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。 内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看编译器对该函数定义的具体处理。 内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益。

5.union型变量

union,共用体,也叫联合体,在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的。union变量所占用的内存长度等于最长的成员的内存长度。 union与struct有几分类似,我们通过对比来说明union的特性。 先看一个关于struct的例子:

1
2
3
4
5
6
struct student
{
char mark;
long num;
float score;
};

sizeof(struct student)的值为12bytes,即mark、num和score三个变量所占空间之和。 下面是关于union的例子:

1
2
3
4
5
6
union test
{
char mark;
long num;
float score;
};

sizeof(union test)的值为4。因为共用体将一个char类型的mark、一个long类型的num变量和一个float类型的score变量存放在同一个地址开始的内存单元中,而char类型和long类型所占的内存字节数是不一样的,但是在union中都是从同一个地址存放的,也就是使用的覆盖技术,这三个变量互相覆盖,而这种使几个不同的变量共占同一段内存的结构,称为“共用体”类型的结构。 因union中的所有成员起始地址都是一样的,所以&a.mark、&a.num和&a.score的值都是一样的。

6.虚函数(virtual function)

在面向对象编程中,虚函数是一种可继承的、可重载的函数。虚函数概念的提出是为了解决以下问题: 在面向对象程序设计中,C++通过虚函数实现多态.”无论发送消息的对象属于什么类,它们均发送具有同一形式的消息,对消息的处理方式可能随接手消息的对象而变”的处理方式被称为多态性。而虚函数是通过Virtual关键字来限定的。 关于虚函数,我们知道在类Base中加了Virtual关键字的函数就是虚拟函数(例如函数print),于是在Base的派生类Derived中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。当基类Base的指针point指向派生类Derived的对象时,对point的print函数的调用实际上是调用了Derived的print函数而不是Base的print函数。这是面向对象中的多态性的体现。(关于虚拟机制是如何实现的,参见Inside the C++ Object Model ,Addison Wesley 1996) 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base  
{
public:Base(){}
public:
virtual void print(){cout<<"Base";}
};
class Derived:public Base
{
public:Derived(){}
public:
void print(){cout<<"Derived";}
};
int main()
{
Base *point=new Derived();
point->print();
}
//---------------------------------------------------------
Output:
Derived

这也许会使人联想到函数的重载,但稍加对比就会发现两者是完全不同的: (1) 重载的几个函数必须在同一个类中; 覆盖的函数必须在有继承关系的不同的类中 (2) 覆盖的几个函数必须函数名、参数、返回值都相同; 重载的函数必须函数名相同,参数不同。参数不同的目的就是为了在函数调用的时候编译器能够通过参数来判断程序是在调用的哪个函数。这也就很自然地解释了为什么函数不能通过返回值不同来重载,因为程序在调用函数时很有可能不关心返回值,编译器就无法从代码中看出程序在调用的是哪个函数了。 (3) 覆盖的函数前必须加关键字Virtual; 重载和Virtual没有任何瓜葛,加不加都不影响重载的运作。 那么,使用虚函数的意义何在?请看下面的例子,这个例子中WindowB和WindowC不同时存在,这在实际应用中是常有的情况,为了节省内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class WindowA  
{
public:
virtual void Draw(){};
};
class WindowB:public WindowA
{
public:
WindowB(){};
void Draw();
};
class WindowC:public WindowA
{
public:
WindowC(){};
void Draw();
};
void WindowB::Draw()
{
画一个圆;
}
void WindowC::Draw()
{
画一个正方形;
}

当在定义WindowA时,不确定它的派生类WindowB和WindowC在Draw函数中要画什么,这时把Draw定义成C++虚函数,在派生类中具体实现。 说到这,读者会问:那我把Draw在WindowB和WindowC中写好,还会少写一个类WindowA。 是的,是少写了,如你所说,你会如此使用:

1
2
3
4
5
6
WindowB* b = new WindowB;  
b->Draw(); //画个圆
delete b;
WindowC* c = new WindowC;
c->Draw(); //画个正方形
delete c;

我这里用指针实现,在上面的代码中,b和c是两个独立的对象的指针。但如果派生类不是一两个,而是几十个,几百个,那你该怎么办呢?在头文件中定义几百个变量? 非也,到时候用C++虚函数的作用了:

1
2
3
4
5
6
7
8
WindowA* a = new WindowsB;  
a->Draw(); //画个圆,此处调用了WindowB中的Draw函数实现
if(a)
delete a; //new 出来的一定要delete
WindowA* a=new WindowC;
a->Draw(); //画个正方形,此处调用了WindowC中的Draw函数实现
if(a)
delete a;

在上面的代码中,a实现了一个中转变量的作用,只要是从WindowA派生的,我都能赋值给a,而b和c都是临时变量。再来多少个WindowA的派生类都没问题了,我只要在头文件中定义一个WindowA的指针变量就行了。

  • 版权声明: 本博客所有文章,未经许可,任何单位及个人不得做营利性使用!转载请标明出处!如有侵权请联系作者。
  • Copyrights © 2015-2020 翟天野

请我喝杯咖啡吧~