C++入门和关键特性总结

来源:互联网 发布:域名纠纷 编辑:程序博客网 时间:2024/04/29 21:33

在这几年使用c++的开发过程中,本人感觉

1、C++的设计很复杂,很多特性非常微妙而且难以掌握,对于应用开发程序员,一般情况下只用到基本的特性,在语言的非本质细节上投入太多精力得不偿失,并且也很难记住这些细节;

2、互联网业务开发中,和开发基础类库不同,速度是第一要务,没有必要使用语言的一些高级技巧,提前考虑扩展性等特性;先完成开发,代码丑点没关系,后面通过重构一步步提升;

3、现在“大系统小做”思想深入人心,单个模块的功能并不复杂,代码量也不大;

故本文总结了一下C++最常用到的一些语言特性,以及新手一些容易遗漏的地方,希望对c++新人有所帮助,如有不当之处,欢迎指出。

当然这些特征还是基于C++03标准的,最新的C++标准已经越来越像动态语言。

 

1. 使用C++风格

C++是以C为基础发展起来的,早期也称为带classC,但经过这些年的发展,C++的特点早已经超过这个范畴,所以如果你写的C++代码还是带classC,就要反思一下自己了。C++发明人Bjarne Stroustrup举了一个例子,如实现字符串连接的功能

C++风格的实现

string compose(const string& name, const string& domain)

{

    return name+'@'+domain;

}

 

C风格的实现

char* compose(const char* name, const char* domain)

{

    char* res = malloc(strlen(name)+strlen(domain)+2); 

    char* p = strcpy(res,name);

    p += strlen(name);

    *p = '@';

    strcpy(p+1,domain);

    return res;

}

可见C++更简单,开发效率显然更高,并且C的这个实现还有陷阱。

 

在网络上另外找了个例子,读入一个小文本文件,行之间按字母排序,然后将排序后的内容写入另一个文件

int main()
{
    ifstream fin("in.txt") ; //输入文件
    vector<string> buf; //缓冲区
    string d; //字符串临时对象
    while(getline(fin,d)) buf.push_back(d) ; //读,并缓冲
    sort(buf.begin() ,buf.end()) ; //排序
    ofstream fout("out.txt") ; //输出文件
    copy(buf.begin() ,buf.end() ,ostream_iterator<string>(fout,"/n")) ; //
}

没有char*[]scanfprintfopenC特色的词汇,同时可以看到,利用STLc++代码开发效率已经相当高。

虽然有了STL,但C++的问题还是基础库太少,结果是每一个团队都发明了很多很多相同的轮子,造成了非常大的资源浪费。

 

2. 对象

a) 四个特殊成员函数

1, 默认构造函数

2, 析构函数

3, 拷贝构造函数

4, 赋值函数

 

C++是面向对象的语言,对象的创建和销毁,以及对象的复制非常重要,这里面的机制一定要弄清楚。有几个值得注意的特点:

1、这四个函数编译器都可以自动生成

2、如果自定义了一个构造函数,则编译器不会自动生成一个默认构造函数

3、如果自定义了拷贝构造函数或赋值函数,则通常情况下也要定义另外一个,换句话说,要成对定义

4、拷贝构造函数和赋值函数非常重要,但我们经常忽略了它们;

5、尽量使用初始化列表给类变量赋值,提高性能。

6、构造时,先构造父类,再构造子类;析构时,先析构子类,再析构父类

 

b) 值语义

C++和其他大部分高级语言不同的一点是,自定义类型的复制默认是值语义的,即默认新建一个副本,新对象和旧对象之间没有关系,如参数传递,显式赋值等。

我们知道,对于基本数据类型如整数等,所有的语言都使用值语义,如i=10, j=i;赋值之后,ij两个变量之间是没有关系的,改变一个不会影响另外一个。而对于自定义类型,则不一样了。

C++的赋值语句b=a结束后,b对象和a对象已经没有关系了。如果要想ba指向同一个对象,则需要显式使用引用和指针。

java则默认是引用语义的,如果执行b=a,则ba指向同一个对象。如果要想获得一个副本,要显式调用clone等方法。

 

C++中,默认使用值语义,对于管理有唯一性特征资源的类型,需要慎重考虑是采用深拷贝、浅拷贝,还是禁止拷贝,如管理着数据库连接的类,或者有指针成员变量的类。

 

咋看起来,引用语义性能更好,但很难说,值语义由于数据locality,数据大部分是聚集在一起的,有利于cache

我认为C++默认使用值语义的原因是没有GC(垃圾自动收集)。如果默认采用引用语义,则到处都是指针,而这些指针需要程序员自己来管理,代码复杂度会迅速提高,很难掌握。

 

 

 

3. 对象生命期

这里生命周期的定义以构造函数生,析构函数死。

a) 全局对象和静态对象

进入main函数前生成,退出main函数后销毁,即在整个运行期都存在;需要注意的一点是,在不同cpp中定义的全局变量,它们的初始化顺序是不做保证的,所以最好不要让它们之间在初始化顺序上有依赖关系。如果有依赖关系,则需要手动采取一些措施来解决。

 

b) 堆对象

new方式分配在heap里的对象,需主动销毁,生命周期由程序员决定;

 

c) 栈对象

自动分配,和作用域相关,对象退出其作用域时会自动销毁,这也是RAII的基础。以如下一段代码来说明:

1   class Book

2   {

3   public: 

4       Book():price(0){}

5       Book(int i):price(i){}

6   

7   private:

8       int price;

9   };

10  

11  Book test(Book b)

12  {

13      Book c = b;

14      {

15          Book d;

16      }

17      return c;    

18  }

19    

20  int main()  

21  {  

22      Book a = Book(1) + Book(2);

23      Book e = test(a);

24  

25      return 0;

26  }

 

下面按执行顺序来说明对象周期的标准做法,但实际执行时不一定如此,因为编译器会做很多优化。如“Book a = b; ”这一个语句,标准做法是先用默认构造函数生成a,然后将b赋值给a,即调用两次函数来生成a对象,但编译器很可能优化成用拷贝构造函数一步到位。

 

21行:Book a = Book(1) + Book(2); 这里会先在栈上生成两个临时对象, 相加后生成一个新对象,然后调用赋值函数生成a。这两个临时对象会在21行执行完毕时销毁

 

22行:调用test函数,将调用拷贝构造函数,在test的执行栈上生成函数的参数对象b

 

13行:正常情况下,会先用默认构造函数在栈上生成对象c,然后调用赋值函数,将b的值赋予c

 

14行:创建了一个新作用域,对象d到了17行,对象d将会销毁

 

17行: 返回对象c。对于返回值,一般的处理过程是这样的,如果返回值的尺寸比较小,如整数,可以放在寄存器里,则使用寄存器传递。如果尺寸比较大,则通过拷贝来传递。在这里例子里,main在调用test时,会在main的栈里分配了一个临时对象temptestreturn c时,将c的值拷贝到temp里,然后再将temp拷贝到对象e

 

 

4. 多态

从字面意义上,多态就是多种形态。多态和类型紧密相关,字面意义上相同的类型,在调用同样的接口时,却呈现出不同的形态。

a) 多态的实现方式

C++有如下两种多态实现方式

 

1、子类型多态

通过继承和虚函数,在运行期动态绑定其实际的接口。

 

2、泛型编程

通过模板的参数多态,在编译期绑定。这种多态其实很常见,如middle框架的service定义,就是使用模板来实现多态。

 

b) 多态的作用

多态带来的好处是同一化处理,也是实现DRYdon’t repeat yourself)原则的重要途径。

 

面向对象的高级语言都支持多态,但在phppython等语言中,大家并不怎么关注多态这个概念,因为这些语言天生就是多态的。

为什么,因为这些语言采用动态类型,在实际实行时,执行环境并不关注对象的类型是什么,只要对象有这个接口,就可以执行,这种情况也称之为鸭子类型。

鸭子类型可以这样表述,“当看到一个东西走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这个东西就可以被称为鸭子。”

 

C++类型是静态类型的,一个对象只能调用它所属类型的接口,多态的引入,既具备静态类型的安全性,又部分具有动态类型的灵活性。

 

c) 虚函数

 

大家都知道调用虚函数时,实际调用的是对象的实际类型里所定义的函数,而不是指针或引用的类型里定义的。那么有一个问题来了,在对象初始化或销毁的过程中,即在构造函数或析构函数里调用自身类型的虚函数,调用的是那一个函数呢?

这里很容易引起混乱,因为在构造函数里,子类还未初始化,而在析构函数里,子类已经销毁了。所以C++明确规定,在构造或析构函数里,调用自身类型虚函数的多态失效,默认调用本class定义的函数,如果本class没有定义,则调用父类的。

在实际编码过程中,最好显式指定调用的是那个class定义的函数。如A::vf1()

 

 

虚函数还有另外一个值得注意的问题,析构函数为什么要声明为虚函数?

其实不是所有析构函数都需要声明为虚函数,而是准备用来继承的class,需要将其析构函数设置为虚函数,原因是,如果一个父类的指针指向一个子类对象,则delete该指针时,调用的是父类的析构函数,如果子类里有需要释放的资源,则这些资源将得不到释放。

而对于没有子类的类,也不打算有子类的类,析构函数就没必要设为虚函数了。

 

 

5. RAII惯用法

RAII全称是“Resource acquisition is initialization”,直译为“资源获取就是初始化”。RAII的核心思想是使用对象管理资源,对象“消亡”则自动释放资源。RAIIC++最核心的特性之一。

 

常见的资源包括文件、网络连接、数据库连接、信号量、事件、线程、内存等,甚至可以是状态。在我们的CGImiddle框架代码里面,大量用到这种技巧。

 

举一个例子,对于在多线程应用中用于同步线程的锁MutexScopedLock类用于实现锁/解锁的操作:

class ScopedLock {  

public:  

explicit ScopedLock (Mutex& m) : mutex(m) { mutex.lock(); locked = true; }  

~ScopedLock () { if (locked) mutex.unlock(); }  

void unlock() { locked = false; mutex.unlock(); }  

private:  

ScopedLock (const ScopedLock&);  

ScopedLock& operator= (const ScopedLock&);  

Mutex& mutex;  

bool locked;  

};

ScopedLock实例对象被创建时,mutex就被锁定了,而当实例作用域生命期结束时mutex隐式释放。通过这种方法避免了忘记释放锁,从而避免了此原因所引起的死锁和崩溃。

 

{  

ScopedLock locker(mtx);  

…  

} // 自动释放

 

 

6. 智能指针

广义上来说,智能指针是RAII中的一种,用于管理对象资源。在C++中,又有其特殊性。为什么C++需要智能指针,而javapython等没有, 这是因为C++缺少GC(垃圾回收机制),引入智能指针相当于在应用代码层面打的一个补丁。

虽然名称是智能,但是在使用上有很多陷阱,特别是在智能指针对象的生命周期管理,拷贝构造以及赋值的操作上,不小心就会出现和原始指针同样的问题。

C++03官方标准库有auto_ptr。最新的标准库有shard_ptr、unique_ptr等。

 

 

7. 异常

对于异常,业界有两种不同的看法,一种是尽量不使用异常,如google C++编程规范;而C++创始人却建议C++的用法是:google 编程规范 异常。

 

异常的好处是可以将业务处理逻辑和错误处理逻辑分开,代码更清晰。某些条件下不使用异常,代码结构会很别扭,如

1、构造函数,如果不能在构造函数里抛出异常,则初始化某些对象时不得不经过两步,第一步是在构造函数里初始化“不重要”的事,然后在一个单独的init()函数里初始化“重要”的事,这带来中间状态;

2、函数只能返回错误码,要想获得真正的返回值,只能用指针或引用将实参传进去。

 

异常主要的问题是

1、要保证异常安全,要注意使用RAII方式保证资源正确释放;

2、代码的运行流更复杂,函数可能在不确定的地方返回,导致代码管理和测试困难。

3、如果未能正确捕获异常,可能导致进程退出,影响后台服务的可用性

 

构造函数可以抛出异常,抛出异常时,不会调用对应的析构函数,因为对象尚未构造完毕。

析构函数绝对不能抛出异常,因为析构函数可能是在一个异常的上下文里,如果再抛出一个异常,则后果是未定义的。

 

 

8. STL

 

a) 容器

放到容器中的对象要有值语义,如果对象的成员变量里有指针或者其他独特性的资源,要特别注意复制时是浅拷贝、深拷贝。如果不能复制,则不能使用stl容器管理。

 

 

b) 迭代器

迭代器是一个抽象化的指针,可以使用迭代器遍历、更新容器。需要注意的时,如果是更新操作,迭代器可能失效。这很明显,某个迭代器指向容器里某个元素的位置,如果这个元素在更新时移动了位置、或者被删除了,则该迭代器将指向一个无效或错误的位置。

 

 

9. 未定义行为

 

因为机器之间的差异、以及因为兼容C而带来的遗留问题,C++标准对很多方面的东西尚没有明确定义,如下面两种情况,我们在实际编码中应该避免出现这种情况,而是用无歧义的方式来实现。

a) 表达式求值

int i = 10;

int j = ++i + i++;

上面的语句执行后,j的值是多少?

答案是没有定义的,C++对表达式里同一优先级的表达式变量,其计算顺序是不做规定的,可能是先计算++i,也可能是先计算i++,这样做的主要目的是方便编译器优化代码。

 

再举个例子

a = p() + q() * r(); 

三个函数p()q()r()可能以6种顺序中的任何一种被评估求值。乘法运算符的高优先级只能保证q()r()的返回值首先相乘,然后再加到p()的返回值上。所以,就算加上再多的括号依旧不能解决问题。

 

所以,如果表达式里的计算有副作用,如i++等,则要小心了,不要想当然以为会默认从左到右计算。

 

b) 函数参数计算顺序

int i = 10;

f(i++, ++i);

上述语句中,实际传给函数f的参数值各是多少?

这也是无定义的,我们知道函数参数是从右到左压栈,就以为参数会从右到左计算。实际上,计算顺序也是没有定义的,先计算参数1或先计算参数2都有可能。

 

0 0