C++入门和关键特性总结
来源:互联网 发布:域名纠纷 编辑:程序博客网 时间:2024/04/29 21:33
在这几年使用c++的开发过程中,本人感觉
1、C++的设计很复杂,很多特性非常微妙而且难以掌握,对于应用开发程序员,一般情况下只用到基本的特性,在语言的非本质细节上投入太多精力得不偿失,并且也很难记住这些细节;
2、互联网业务开发中,和开发基础类库不同,速度是第一要务,没有必要使用语言的一些高级技巧,提前考虑扩展性等特性;先完成开发,代码丑点没关系,后面通过重构一步步提升;
3、现在“大系统小做”思想深入人心,单个模块的功能并不复杂,代码量也不大;
故本文总结了一下C++最常用到的一些语言特性,以及新手一些容易遗漏的地方,希望对c++新人有所帮助,如有不当之处,欢迎指出。
当然这些特征还是基于C++03标准的,最新的C++标准已经越来越像动态语言。
1. 使用C++风格
C++是以C为基础发展起来的,早期也称为带class的C,但经过这些年的发展,C++的特点早已经超过这个范畴,所以如果你写的C++代码还是带class的C,就要反思一下自己了。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*[],scanf,printf,open等C特色的词汇,同时可以看到,利用STL,c++代码开发效率已经相当高。
虽然有了STL,但C++的问题还是基础库太少,结果是每一个团队都发明了很多很多相同的轮子,造成了非常大的资源浪费。
2. 对象
a) 四个特殊成员函数
1, 默认构造函数
2, 析构函数
3, 拷贝构造函数
4, 赋值函数
C++是面向对象的语言,对象的创建和销毁,以及对象的复制非常重要,这里面的机制一定要弄清楚。有几个值得注意的特点:
1、这四个函数编译器都可以自动生成
2、如果自定义了一个构造函数,则编译器不会自动生成一个默认构造函数
3、如果自定义了拷贝构造函数或赋值函数,则通常情况下也要定义另外一个,换句话说,要成对定义
4、拷贝构造函数和赋值函数非常重要,但我们经常忽略了它们;
5、尽量使用初始化列表给类变量赋值,提高性能。
6、构造时,先构造父类,再构造子类;析构时,先析构子类,再析构父类
b) 值语义
C++和其他大部分高级语言不同的一点是,自定义类型的复制默认是值语义的,即默认新建一个副本,新对象和旧对象之间没有关系,如参数传递,显式赋值等。
我们知道,对于基本数据类型如整数等,所有的语言都使用值语义,如i=10, j=i;赋值之后,i和j两个变量之间是没有关系的,改变一个不会影响另外一个。而对于自定义类型,则不一样了。
C++的赋值语句b=a结束后,b对象和a对象已经没有关系了。如果要想b和a指向同一个对象,则需要显式使用引用和指针。
而java则默认是引用语义的,如果执行b=a,则b和a指向同一个对象。如果要想获得一个副本,要显式调用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的栈里分配了一个临时对象temp,test在return c时,将c的值拷贝到temp里,然后再将temp拷贝到对象e。
4. 多态
从字面意义上,多态就是多种形态。多态和类型紧密相关,字面意义上相同的类型,在调用同样的接口时,却呈现出不同的形态。
a) 多态的实现方式
C++有如下两种多态实现方式
1、子类型多态
通过继承和虚函数,在运行期动态绑定其实际的接口。
2、泛型编程
通过模板的参数多态,在编译期绑定。这种多态其实很常见,如middle框架的service定义,就是使用模板来实现多态。
b) 多态的作用
多态带来的好处是同一化处理,也是实现DRY(don’t repeat yourself)原则的重要途径。
面向对象的高级语言都支持多态,但在php、python等语言中,大家并不怎么关注多态这个概念,因为这些语言天生就是多态的。
为什么,因为这些语言采用动态类型,在实际实行时,执行环境并不关注对象的类型是什么,只要对象有这个接口,就可以执行,这种情况也称之为鸭子类型。
鸭子类型可以这样表述,“当看到一个东西走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这个东西就可以被称为鸭子。”
而C++类型是静态类型的,一个对象只能调用它所属类型的接口,多态的引入,既具备静态类型的安全性,又部分具有动态类型的灵活性。
c) 虚函数
大家都知道调用虚函数时,实际调用的是对象的实际类型里所定义的函数,而不是指针或引用的类型里定义的。那么有一个问题来了,在对象初始化或销毁的过程中,即在构造函数或析构函数里调用自身类型的虚函数,调用的是那一个函数呢?
这里很容易引起混乱,因为在构造函数里,子类还未初始化,而在析构函数里,子类已经销毁了。所以C++明确规定,在构造或析构函数里,调用自身类型虚函数的多态失效,默认调用本class定义的函数,如果本class没有定义,则调用父类的。
在实际编码过程中,最好显式指定调用的是那个class定义的函数。如A::vf1()
虚函数还有另外一个值得注意的问题,析构函数为什么要声明为虚函数?
其实不是所有析构函数都需要声明为虚函数,而是准备用来继承的class,需要将其析构函数设置为虚函数,原因是,如果一个父类的指针指向一个子类对象,则delete该指针时,调用的是父类的析构函数,如果子类里有需要释放的资源,则这些资源将得不到释放。
而对于没有子类的类,也不打算有子类的类,析构函数就没必要设为虚函数了。
5. RAII惯用法
RAII全称是“Resource acquisition is initialization”,直译为“资源获取就是初始化”。RAII的核心思想是使用对象管理资源,对象“消亡”则自动释放资源。RAII是C++最核心的特性之一。
常见的资源包括文件、网络连接、数据库连接、信号量、事件、线程、内存等,甚至可以是状态。在我们的CGI和middle框架代码里面,大量用到这种技巧。
举一个例子,对于在多线程应用中用于同步线程的锁Mutex,ScopedLock类用于实现锁/解锁的操作:
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++需要智能指针,而java、python等没有, 这是因为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都有可能。
- C++入门和关键特性总结
- iOS7新特性 ViewController转场切换(一) 以前总结和关键API介绍
- C关键字和关键语句
- C语言深度解剖 关键知识总结
- c#:特性和属性
- “FCoE全解系列”之关键特性和技术分析
- Java Struts 特性和新特性总结
- ESB关键特性
- innodb 关键特性
- ClustrixDB-关键特性
- InnoDB关键特性
- InnoDB的关键特性
- InnoDB关键特性 Doublewrite
- RocketMQ 关键特性
- RocketMQ 关键特性
- 【Lua】特性和一些基础语法总结(Lua入门到精通一)
- Ojbective-c 入门总结
- C语言入门总结!
- 环信iOS SKD 3.1.0集成总结
- 平衡二叉树
- 机器学习:EM算法
- jsoup 和nekohtml,htmlparser解析html
- 画板 ios
- C++入门和关键特性总结
- 7. Reverse Integer
- OpenGL两种投影方式
- GitHub上传本地项目 之 Github设置SSH keys (1)
- 第四周 使用API和C编码中的嵌入式汇编 来应用同一个系统调用
- 单例模式最佳写法
- Eclipse中用Tomcat发布的Web项目存放路径
- 运用 jsoup 对 HTML 文档进行解析和操作
- Gym 100015B Ball Painting