宏的好处与坏处html
宏的好处:#与##的使用ios
三个有用的特征:字符串定义、字符串串联和标志粘贴。编程
字符串定义的完成是用#指示,它允许设一个标识符并把它转化为字符串,然而字符串串联发生在当两个相邻的字符串没有分隔符时,在这种状况下字符串组合在一块儿。在写调试代码时,这两个特征是很是有效的。数组
#define DEBUG(X) cout<<#X " = " << X << endl
上面的这个定义能够打印任何变量的值。安全
咱们也能够获得一个跟踪信息,在此信息里打印出它们执行的语句。框架
#define TRACE(S) cout << #S << endl; S
#S
定义了要输出的语句。第2个S重申了语句,因此这个语句被执行。固然,这可能会产生问题,尤为是在一行for循环中。ide
for (int i = 0 ; i < 100 ; i++ ) TRACE(f(i)) ;
由于在TRACE( )宏里实际上有两个语句,因此一行for循环只执行第一个。函数
for (int i = 0 ; i < 100 ; i++ ) cout << "f(i)" << endl; f(i); // 第二条语句脱离了for循环,所以执行不到
解决方法是在宏中用逗号代替分号。优化
标志粘贴在写代码时是很是有用的,用##表示。它让咱们设两个标识符并把它们粘贴在一块儿自动产生一个新的标识符。例如:this
#define FIELD(A) char *A##_string;int A##_size
此时下面的代码:
class record{ FIELD(one); FIELD(two); FIELD(three); //... };
就至关于下面的代码:
class record{ char *one_string,int one_size; char *two_string,int two_size; char *three_string,int three_size; //... };
宏的很差:容易出错
下面举个例子便可说明:
#define band(x) (((x)>5 && (x)<10) ? (x) :0) int main() { for(int i = 4; i < 11; i++) { int a = i; cout << "a = " << a << "\t"; cout << "band(++a)" << band(++a) << "\t"; cout << "a = " << a << endl; } return 0; }
输出:
a = 4 band(++a)0 a = 5
a = 5 band(++a)8 a = 8
a = 6 band(++a)9 a = 9
a = 7 band(++a)10 a = 10
a = 8 band(++a)0 a = 10
a = 9 band(++a)0 a = 11
a = 10 band(++a)0 a = 12
存储类型指定符
经常使用的有static
和extern
。
不经常使用的有两个:一是auto
,人们几乎不用它,由于它告诉编译器这是一个局部变量,实际上编译器老是能够从 变量定义时的上下文中判断出这是一个局部变量。因此auto
是多余的。还有一个是register
,它也是局部变量,但它告诉编译器这个特殊的变量要常常用到,因此编译器应该尽量地让它保存在寄存器中。它用于优化代码。各类编译器对这种类型的变量处理方式也不尽相同,它们有时会忽略这种存储类型的指定。通常,若是要用到这个变量的地址, register
指定符一般都会被忽略。应该避免用register
类型,由于编译器在优化代码方面一般比咱们作得更好。
位拷贝(bitcopy)与值拷贝的区别(很重要)
由1个例子来讲明:一个类在任什么时候候知道它存在多少个对象,能够经过包含一个static成员来作到,以下代码所示:
#include <iostream> using namespace std; class test { static int object_count; public: test() { object_count++; print("test()"); } static void print(const char *msg = 0) { if(msg) cout << msg << ": "; cout << "object_count = " << object_count << endl; } ~test() { object_count--; print("~test()"); } }; int test::object_count = 0; // pass and return by value. test f(test x) { x.print("x argument inside f()"); return x; } int main() { test h; test::print("after construction of h"); test h2 = f(h); test::print("after call to f()"); return 0; }
然而输出并非咱们指望的那样:
test(): object_count = 1
after construction of h: object_count = 1
x argument inside f(): object_count = 1
~test(): object_count = 0
after call to f(): object_count = 0
~test(): object_count = -1
~test(): object_count = -2
在h生成之后,对象数是1,这是对的。咱们但愿在f()调用后对象数是2,由于h2也在范围内。然而,对象数是0,这意味着发生了严重的错误。这从结尾两个析构函数执行后使得对象数变为负数的事实获得确认,有些事根本就不该该发生。
让咱们来看一下函数f()经过传值方式传入参数那一处。原来的对象h存在于函数框架以外,同时在函数体内又增长了一个对象,这个对象是传值方式传入的对象的拷贝,这属于位拷贝,调用的是默认拷贝构造函数,而不是调用构造函数。然而,参数的传递是使用C的原始的位拷贝的概念,但test类须要真正的初始化来维护它的完整性。因此,缺省的位拷贝不能达到预期的效果。
当局部对象出了调用的函数f()范围时,析构函数就被调用,析构函数使object_count减少。 因此,在函数外面, object_count等于0。h2对象的建立也是用位拷贝产生的(也是调用默认拷贝构造函数),因此,构造函数在这里也没有调用。当对象h和h2出了它们的做用范围时,它们的析构函数又使object_count值变为负值。
总结:
- 位拷贝拷贝的是地址(也叫浅拷贝),而值拷贝则拷贝的是内容(深拷贝)。
- 深拷贝和浅拷贝能够简单理解为:若是一个类拥有资源,当这个类的对象发生复制过程的时候,资源从新分配,这个过程就是深拷贝,反之,没有从新分配资源,就是浅拷贝。
- 默认的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,假若类中含有指针变量,这两个函数注定将出错。
关于位拷贝和值拷贝的深刻理解能够参考这篇文章:C++中的位拷贝与值拷贝浅谈
为了达到咱们指望的效果,咱们必须本身定义拷贝构造函数:
test(const test& t) { object_count++; print("test(const test&)"); }
这样输出才正确:
test(): object_count = 1
after construction of h: object_count = 1
test(const test&): object_count = 2
x argument inside f(): object_count = 2
test(const test&): object_count = 3
~test(): object_count = 2
after call to f(): object_count = 2
~test(): object_count = 1
~test(): object_count = 0
若是在main中加一句“f(h);”,即忽略返回值,那么返回的时候还会调用拷贝构造函数吗?
答案是:会调用。这时候会产生一个临时对象,因为该临时对象没有用处,所以会立刻调用析构函数销毁掉。这时候输出就会像下面这样:
test(): object_count = 1
after construction of h: object_count = 1
test(const test&): object_count = 2
x argument inside f(): object_count = 2
test(const test&): object_count = 3
~test(): object_count = 2
after call to f(): object_count = 2
test(const test&): object_count = 3
x argument inside f(): object_count = 3
test(const test&): object_count = 4
~test(): object_count = 3
~test(): object_count = 2
~test(): object_count = 1
~test(): object_count = 0
若是一个类由其它几个类的对象组合而成,若是此时该类没有自定义拷贝构造函数,那么编译器递归地为全部的成员对象和基本类调用拷贝构造函数。若是成员对象也含有别的对象,那么后者的拷贝构造函数也将被调用。
怎样避免调用拷贝构造函数?仅当准备用传值的方式传递类对象时,才须要拷贝构造函数。有两种解决方法:
class noCC { int i; noCC(const noCC&); // private and no definition public: noCC(int I = 0) : i(I) {} }; void f(noCC); main() { noCC n; //! f(n); // error: copy-constructor called //! noCC n2 = n; // error: c-c called //! noCC n3(n); // error: c-c called }
注意这里`n2 = n`也调用拷贝构造函数,注意这里要和赋值函数区分。
void get(const Slice&);
非自动继承的函数
构造函数、析构函数和赋值函数(operator=)不能被继承。
私有继承的目的private
继承的目的是什么,由于在类中选择建立一个private
对象彷佛更合适。将private继承包含在该语言中只是为了语言的完整性。可是,若是没有其余理由,则应当减小混淆,因此一般建议用private
成员而不是private
继承。
然而,private
继承也不是一无用处。
这里可能偶然有这种状况,便可能想产生像基类接口同样的接口,而不容许处理该对象像处理基类对象同样。private继承提供了这个功能。
能对私有继承成员公有化吗?
当私有继承时,基类的全部public成员都变成了private。若是但愿它们中的任何一个是可视的,能够办到吗?答案是能够的,只要用派生类的public选项声明它们的名字便可(新的标准中使用using关键字)。
#include <iostream> class base { public: char f() const { return 'a'; } int g() const { return 2; } float h() const { return 3.0; } }; class derived : base { public: using base::f; // Name publicizes member using base::h; }; int main() { derived d; d.f(); d.h(); // d.g(); // error -- private function return 0; }
这样,若是想要隐藏这个类的基类部分的功能,则private
继承是有用的。
多重继承注意向上映射的二义性。好比base(有个f()方法)有两个子对象d1和d2,且都重写了base的f()方法,此时子类dd若是也有f()方法则不能同时继承自d1和d2,由于f()方法存在二义性,不知道该继承哪一个f()方法。
解决方法是对dd类中的f()方法从新定义以消除二义性,好比明确指定使用d1的f()方法。
固然也不能将dd类向上映射为base类,这能够经过使用虚继承解决,关键字virtual,base中的f()方法改为虚函数且d1和d2的继承都改成虚继承,固然dd继承d1和d2用public继承便可。
C语言中如何关闭assert断言功能?头文件:<assert.h>或<cassert>
在开发过程当中,使用它们,完成后用#define NDEBUG使之失效,以便推出产品,注意必须在头文件以前关闭才有效。
#define NDEBUG #include <cassert>
C++中为了实现多态,编译器对每一个包含虚函数的类建立一个表(称为VTABLE,虚表)。在 VTABLE中,编译器放置特定类的虚函数地址。在每一个带有虚函数的类中,编译器秘密地置一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。经过基类指针作虚函数调用时(也就是作多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。
为每一个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,全部这些都是自动发生的,因此咱们没必要担忧这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的状况下。
在vtable表中,编译器放置了在这个类中或在它的基类中全部已声明为virtual
的函数的地址。若是在这个派生类中没有对在基类中声明为virtual
的函数进行从新定义,编译器就使用基类的这个虚函数地址。
下面举个例子说明:
#include <iostream> enum note { middleC, Csharp, Cflat }; class instrument { public: virtual void play(note) const { cout << "instrument::play" << endl; } virtual char* what() const { return "instrument"; } // assume this will modify the object: virtual void adjust(int) {} }; class wind : public instrument { public: void play(note) const { cout << "wind::play" << endl; } char* what() const { return "wind"; } void adjust(int) {} }; class percussion : public instrument { public: void play(note) const { cout << "percussion::play" << endl; } char* what() const { return "percussion"; } void adjust(int) {} }; class string : public instrument { public: void play(note) const { cout << "string::play" << endl; } char* what() const { return "string"; } void adjust(int) {} }; class brass : public wind { public: void play(note) const { cout << "brass::play" << endl; } char* what() const { return "brass"; } }; class woodwind : public wind { public: void play(note) const { cout << "woodwind::play" << endl; } char* what() const { return "woodwind"; } }; instrument *A[] = { new wind, new percussion, new string, new brass };
下图画的是指针数组A[]。
下面看到的是经过instrument指针对于brass调用adjust()。instrument引用产生以下结果:
编译器从这个instrument指针开始,这个指针指向这个对象的起始地址。全部的instrument对象或由instrument派生的对象都有它们的VPTR,它在对象的相同的位置(经常在对象的开头),因此编译器可以取出这个对象的VPTR。VPTR指向VTABLE的开始地址。全部的VTABLE有相同的顺序,无论何种类型的对象。 play()是第一个,what()是第二个,adjust()是第三个。因此编译器知道adjust()函数必在VPTR + 2处。这样,不是“以instrument :: adjust地址调用这个函数”(这是早捆绑,是错误活动),而是产生代码,“在VPTR + 2处调用这个函数”。由于VPTR的效果和实际函数地址的肯定发生在运行时,因此这样就获得了所但愿的晚捆绑。向这个对象发送消息,这个对象能判定它应当作什么。
当多态地处理对象时,传地址与传值有明显的不一样。全部在这里已经看到的例子和将会看到的例子都是传地址的,而不是传值的。这是由于地址都有相同的长度,传派生类型(它一般稍大一些)对象的地址和传基类(它一般小一点)对象的地址是相同的。如前面解释的,使用多态的目的是让对基类对象操做的代码也能操做派生类对象。
若是使用对象而不是使用地址或引用进行向上映射,发生的事情会使咱们吃惊:这个对象 被“切片”,直到所剩下来的是适合于目的的子对象。在下面例子中能够看到经过检查这个对象的长度切片剩下来的部分。
#include <iostream> using namespace std; class base { int i; public: base(int I = 0) : i(I) {} virtual int sum() const { return i; } }; class derived : public base { int j; public: derived(int I = 0, int J = 0) : base(I), j(J) {} virtual int sum() const { return base::sum() + j; } }; void call(base b) { cout << "sum = " << b.sum() << endl; } main() { base b(10); derived d(10, 47); call(b); call(d); }
函数call( )经过传值传递一个类型为base的对象。而后对于这base对象调用虚函数sum( )。 咱们可能但愿第一次调用产生10,第二次调用产生57。实际上,两次都产生10。 在这个程序中,有两件事情发生了。
另外,用值传递时,它对base对象使用拷贝构造函数,该构造函数初始化vptr指向base vtable,而且只拷贝这个对象的base部分。这里没有显式的拷贝构造函数,因此编译器自动地为咱们合成一个。因为上述诸缘由,这个对象在切片期间变成了一个base对象。
对象切片其实是去掉了对象的一部分,而不是象使用指针或引用那样简单地改变地址的内容。所以,对象向上映射不常作,事实上,一般要提防或防止这种操做。咱们能够经过在基 类中放置纯虚函数来防止对象切片。这时若是进行对象切片就将引发编译时的出错信息。
最后注意:虚机制在构造函数中不工做。即在构造函数中调用虚函数没有结果。
概念
运行时类型识别(Run-time type identification, RTTI)是在咱们只有一个指向基类的指针或引用时肯定一个对象的准确类型。
使用方法
通常状况下,咱们并不须要知道一个类的确切类型,虚函数机制能够实现那种类型的正确行为。可是有些时候,咱们有指向某个对象的基类指针,肯定该对象的准确类型是颇有用的。
RTTI与异常同样,依赖驻留在虚函数表中的类型信息。若是试图在一个没有虚函数的类上用RTTI,就得不到预期的结果。
RTTI的两种使用方法
typeid()
,就像sizeof()
同样,看上都像一个函数。但实际上它是由编译器实现的。typeid()
带有一个参数,它能够是一个对象引用或指针,返回全局typeinfo
类的常量对象的一个引用。能够用运算符“==”和“!=”来互相比较这些对象。也能够用name()
来得到类型的名称。注意,若是给typeid( )
传递一个shape*
型参数,它会认为类型为shape*
,因此若是想知道一个指针所指对象的精确类型,咱们必须逆向引用这个指针。好比,s是个shape*
, 那么: cout << typeid(*s).name()<<endl;将显示出s所指向的对象类型。
before(typeinfo&)
查询一个typeinfo
对象是否在另外一个typeinfo
对象的前面(以定义实现的排列顺序),它将返回true或false。若是写: if(typeid(me).before(typeid(you))) //...那么表示咱们正在查询me在排列顺序中是否在you以前。
dynamic_cast<>
模板。两种方法的使用举例以下:
#include <iostream> #include <typeinfo> using namespace std; class base { int i; public: base(int I = 0) : i(I) {} virtual int sum() const { return i; } }; class derived : public base { int j; public: derived(int I = 0, int J = 0) : base(I), j(J) {} virtual int sum() const { return base::sum() + j; } }; main() { base *b = new derived(10, 47); // rtti method1 cout << typeid(b).name() << endl; // P4base cout << typeid(*b).name() << endl; // 7derived if(typeid(b).before(typeid(*b))) cout << "b is before *b" << endl; else cout << "*b is before b" << endl; // rtti method2 derived *d = dynamic_cast<derived*>(d); if(d) cout << "cast successful" << endl; }
注意1:这里若是没有多态机制,则RTTI可能运行的结果不是咱们想要的,好比若是没有虚函数,则这里两个都显示base,通常但愿RTTI用于多态类。
注意2:运行时类型的识别对一个void
型指针不起做用。void *
确实意味着“根本没有类型信息”。
void *v = new stimpy; stimpy* s = dynamic_cast<stimpy*>(v); // error cout << typeid(*v).name() << endl; // error
RTTI的实现
典型的RTTI是经过在VTABLE中放一个额外的指针来实现的。这个指针指向一个描述该特定类型的typeinfo结构(每一个新类只产生一个typeinfo的实例),因此typeid( )表达式的做用实际上很简单。VPTR用来取typeinfo的指针,而后产生一个结果typeinfo结构的一个引用—这是一个决定性的步骤—咱们已经知道它要花多少时间。
对于dynamic_cast<目标* > <源指针>
,多数状况下是很容易的,先恢复源指针的RTTI信息再取出目标*
的类型RTTI信息,而后调用库中的一个例程判断源指针是否与目标*
相同或者是目标*
类型的基类。它可能对返回的指针作了一点小的改动,由于目的指针类可能存在多重继承的状况,而源指针类型并非派生类的第一个基类。在多重继承时状况会变得复杂些,由于一个基类在继承层次中可能出现一次以上,而且可能有虚基类。
用于动态映射的库例程必须检查一串长长的基类列表,因此动态映射的开销比typeid()
要大(固然咱们获得的信息也不一样,这对于咱们的问题来讲可能很关键),而且这是非肯定性的,由于查找一个基类要比查找一个派生类花更多的时间。另外动态映射容许咱们比较任何类型,不限于在同一个继承层次中比较两个类。这使得动态映射调用的库例程开销更高了。
映射类型 | 含义 |
---|---|
static_cast | 为了“行为良好”和“行为较好”而使用的映射,包括一些咱们可能如今不用的映射(如向上映射和自动类型转换) |
const_cast | 用于映射常量和变量(const和volatile) |
const_cast | 为了安全类型的向下映射(本章前面已经介绍) |
reinterpret_cast | 为了映射到一个彻底不一样的意思。这个关键词在咱们须要把类型映射回原有类型时要用到它。咱们映射到的类型仅仅是为了故弄玄虚和其余目的。这是全部映射中最危险的 |
注意:若是想把一个const 转换为非const,或把一个volatile转换成一个非volatile(勿遗忘这种状况),就要用到const_cast。这是能够用const_cast的惟一转换。若是还有其余的转换牵涉进来,它必须分开来指定,不然会有一个编译错误。