2011 年,C++ 标准委员会发布了 ISO C++ 标准的一个重要修订版
C++ 11 ;该修订版是 C++
语言演进过程当中的重要一步,也是当前获得编译器(GCC 、LLVM 、Qt5 、Visual
C++ 支持较多,兼容性最为优秀的一个版本。增添了类型说明符auto
和decltype
、Lambda 表达式
、智能指针 unique_ptr shared_ptr weak_ptr
、空指针 nullptr
等诸多新特性,语言风格更加灵活统一的同时,极大提升了程序的编写效率。
本文基于《C++
Primer》 一书最新的第 5 版撰写而成,该书作为 C++
语言学习的经典读物,同样与时俱进增添了 C++ 11
的诸多新特性。因此,本文也选择了支持 C++ 11 标准的 Qt5
作为开发编译环境。由于 C++ 面向过程的语法与 C
语言类似,而笔者之前已经在《Linux C
标准程序设计》 一文对相关内容进行了详尽的表述,因而本文将会着重笔墨水介绍
C++ 面向对象以及标准库方面的内容。
输入输出流
C++
语言并未定义任何输入输出语句,而是通过附加的iostream
标准库提供
IO
机制,该库包含输入流 istream
和输出流 ostream
两种基本类型。流 (Stream)本质上是一个随着时间推移,顺序生成或消耗的字符序列。iostream
标准库当中一共定义了
4 个 IO 对象:
cin
:
标准输入 流(istream
类型);
cout
:标准输出 流(ostream
类型);
cerr
:标准错误 流(ostream
类型);
clog
:标准日志 流(ostream
类型);
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> int main (int argc, char *argv[]) { int input1 = 0 , input2 = 0 ; std::cout << "Enter two numbers : " << std::endl; std::cin >> input1 >> input2; std::cerr << "input1 with cerr is " << input1 << std::endl; std::clog << "input2 with clog is " << input2 << std::endl; return 0 ; }
注意 :如果需要通过控制台打印中文信息,那么可以将 Qt
Creator 的文本编码设置为【System】。
上述代码当中,endl
是一个称为操纵符的特殊值,主要用于结束当前行的输出 ,并将缓冲区(buffer)里的内容刷新至显示设备,这种刷新操作可以确保将程序产生的所有输出信息都写入至输出流。
命名空间 (namespace)可以避免代码中出现命名冲突,cin
、cout
、cerr
、clog
和endl
等标准库成员都定义在名为std
的命名空间 当中,因此代码里需要使用作用域运算符 ::
显式的指定命名空间。
此外,输出运算符 <<
用于将右侧的输出值写入到左侧的ostream
对象当中,并返回写入内容的ostream
对象,从而形成链式调用。而输入运算符 >>
正好相反,将右侧的输入值写入到左侧的istream
对象当中,然后同样将该对象返回以实现链式调用。
命名空间
命名空间用于区分不同作用域当中相同名称的变量、函数或者类,定义命名空间需要使用namespace
关键字,后面紧跟命名空间的名称:
然后,通过命名空间名称::变量/函数/类
即可调用带有命名空间的变量、函数以及类。
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 26 27 28 29 30 31 32 #include <string> #include <iostream> namespace Hank { std::string birthday= "2020" ; void sayHello () { std::cout << "Hello World" << std::endl; }; class Person { public : std::string name; int age; }; } int main (int argc, char *argv[]) { std::cout<< Hank::birthday << std::endl; Hank::sayHello (); Hank::Person person; person.name = "Hank" ; person.age = 35 ; std::cout<< person.name << "-" << person.age << std::endl; return 0 ; };
1 2 3 2020 Hello World Hank-35
上述代码分别使用了标准库定义的std
和自定义的Hank
两个命名空间,这种方式需要为所有变量都添加专属的命名空间名称,这样编写代码显得较为繁琐。面对这种情况,可以使用更为便捷的using
关键字指定后续代码所处的命名空间。
下面的代码,在开头处声明了using namespace std;
,这样在使用cout
和endl
时就无需再分别添加std::
前缀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> using namespace std;int main (int argc, char *argv[]) { char hello[] = "Hello Qt5! Hello " ; char name[5 ]; cin >> name; cout << hello << name << endl; return 0 ; };
针对每条using
可以只引入命名空间当中的一个成员,例如将std
标准库当中的名字全部通过using
关键字声明出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> using std::cout;using std::cin;using std::endl;int main (int argc, char *argv[]) { char hello[] = "Hello Qt5! Hello " ; char name[5 ]; cin >> name; cout << hello << name << endl; return 0 ; };
上述代码声明命名空间以后,再使用cin
、cout
、endl
时就不需要再添加std::
前缀。
注意 :位于头文件的代码通常不应该使用using
声明。因为头文件的内容会拷贝到所有引用它的源文件当中,导致头文件里的using
声明,与源文件的命名空间发生冲突。
命名空间可以由几个单独定义的部分构成 ,甚至一个命名空间的各个组成部分可以分散在多个源文件当中 。如果命名空间中的某个构成部分需要使用定义在其它源文件里的成员,则仍然需要在当前源文件声明该命名空间。除此之外,命名空间还可以进行嵌套 ,即可以在一个命名空间中定义另外的命名空间:
1 2 3 4 5 6 7 8 9 namespace 命名空间1 { namespace 命名空间2 { } } using namespace 命名空间1 ; using namespace 命名空间1 ::命名空间2 ;
下面是一个嵌套命名空间的完整示例,声明using namespace Space1::Space2;
以后最终调用的是Space2
命名空间中的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std;namespace Space1{ void func () { cout << "Space1" << endl; } namespace Space2{ void func () { cout << "Space2" << endl; } } } using namespace Space1::Space2;int main () { func (); return 0 ; }
数据类型
C++ 11 当中出现的数据类型,基本与
C99
标准保持一致,例如两者都兼容long long
数据类型,C++
11 将 C99
中需要通过#include <stdbool.h>
支持的_Bool
升级为原生的bool
关键字,并额外提供了wchar_t
、char16_t
、char32_t
三种针对字符集编码的全新数据类型。
bool
布尔型
1
bytes
char
字符型
1
bytes
wchar_t
宽字符型
2
bytes
char16_t
Unicode 字符型
2
bytes
char32_t
Unicode 字符
4
bytes
short
短整型
2
bytes
int
整型
4
bytes
long
长整型
4
bytes
long long
长长整型
8
bytes
float
单精度浮点型
4
bytes
double
双精度浮点型
8
bytes
long double
长双精度浮点型
12
bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std;int main () { cout << "bool " << sizeof (bool ) << " bytes" << endl; cout << "char " << sizeof (char ) << " bytes" << endl; cout << "wchar_t " << sizeof (wchar_t ) << " bytes" << endl; cout << "char16_t " << sizeof (char16_t ) << " bytes" << endl; cout << "char32_t " << sizeof (char32_t ) << " bytes" << endl; cout << "short " << sizeof (short ) << " bytes" << endl; cout << "int " << sizeof (int ) << " bytes" << endl; cout << "long " << sizeof (long ) << " bytes" << endl; cout << "long long " << sizeof (long long ) << " bytes" << endl; cout << "float " << sizeof (float ) << " bytes" << endl; cout << "double " << sizeof (double ) << " bytes" << endl; cout << "long double " << sizeof (long double ) << " bytes" << endl; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 bool 1 bytes char 1 bytes wchar_t 2 bytes char16_t 2 bytes char32_t 4 bytes short 2 bytes int 4 bytes long 4 bytes long long 8 bytes float 4 bytes double 8 bytes long double 12 bytes
注意 :整型 、字符型 还可以使用signed
(有符号类型)、unsigned
(无符号类型)修饰符,表示该变量的二进制存储形式是否采用符号位 。
布尔类型 bool
C++
里的布尔类型只有true
和false
两个可选值,其底层本质上保存的分别是整型数据1
和0
,但是仅占用
1 个字节的存储空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> using namespace std;int main () { bool no = false ; bool yes = true ; cout << (bool )no << endl; cout << (bool )yes << endl; return 0 ; };
宽字符类型 wchar_t
宽字符类型wchar_t
用于表达宽字符串 (例如:日文系统字符集),其与char
类型一样底层存储的是整型数据,但拥有更大的存储空间,进而可以表达更多的字符编码。代码中,可以通过前缀L
来表示宽字符常量和宽字符串,下面代码将字母H
和单词Hank
以wchar_t
类型进行输出:
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> using namespace std;int main () { wchar_t wc = L'H' ; wcout << wc << endl; wcout << L"Hank" << endl; return 0 ; }
Unicode 类型 char16_t 和
char32_t
C++ 11
新增的char16_t
、char32_t
数据类型可以用于表达更加庞大的
Unicode
字符集 (UTF-16、UTF-32)。其中,char16_t
长度为 16
位,char32_t
长度为 32 位,两者都是无符号的。C++ 11
使用小写字母u
作为前缀表示char16_t
类型的字符或者字符串常量,例如:u'A'
或者u"Hank"
;使用大写字母U
作为前缀表示char32_t
类型的字符或者字符串常量,例如:U'B'
或者U"uinika"
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> using namespace std;int main (int argc, char *argv[]) { char16_t char16 = u'A' ; char16_t string16[] = u"Hank" ; cout << (char )char16 << endl; cout << (char )string16[0 ] << endl; char32_t char32 = U'B' ; char32_t string32[] = U"Uinika" ; cout << (char )char32 << endl; cout << (char )string32[0 ] << endl; return 0 ; }
string 类
ISO/ANSI C++98 标准添加了string
类对 C++
库进行了扩展,因此可以使用string
对象类型的变量而非字符数组 来存储字符串。使用string
类时,必须在程序中包含头文件#include <string>
,且由于string
类位于std
命名空间,因而还需要额外声明using namespace std
,或者直接采用std:string
方式进行引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> #include <string> using namespace std;int main (int argc, char *argv[]) { string hello1 = "Hello Qt! " ; string hello2 ("Hello Hank! " ) ; string hello3 (5 , 'U' ) ; cout << hello1 << hello2 << hello3 << endl; return 0 ; };
1 Hello Qt! Hello Hank! UUUUU
string
类型不能像 Java
那样使用+
运算符进行拼接,而需要通过调用string
类的append()
函数来完成。接下来修改代码,以append()
方式实现与上面代码相同的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> #include <string> using namespace std;int main (int argc, char *argv[]) { string hello1 = "Hello Qt! " ; string hello2 ("Hello Hank! " ) ; string hello3 (5 , 'U' ) ; cout << hello1. append (hello2).append (hello3) << endl; return 0 ; };
运算符==
和!=
分别用于检验string
对象的相等性,两个string
对象相等意味着其长度和包含的字符都相同。关系运算符<
、<=
、>
、>=
则用于检验string
对象的字典顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> #include <string> using namespace std;int main (int argc, char *argv[]) { string A = "Hank" ; string B = "Uinika" ; cout << (A != B) << endl; cout << (A == B) << endl; cout << (A <= B) << endl; return 0 ; };
注意 :标准输出中可以使用boolalpha
为字符串流设置boolalpha
格式标记,让布尔值能以true
或者false
的格式进行显示,而noboolalpha
则用于清除字符串流当中的boolalpha
格式标志;
函数
内联函数
C++
内联函数能够让编译器使用相应的函数代码替换函数调用,这样程序无需跳转至另一个函数位置执行代码,然后再跳转回调用位置,这样内联的函数执行速度会相对更快。使用时需要在声明和定义函数时都添加inline
关键字。
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> #include <string> using namespace std;inline string hello () { return "Hello Qt!" ; }int main (int argc, char *argv[]) { cout << hello () << endl; return 0 ; }
注意 :当开发人员显式请求将函数作为内联函数时,编译器并不一定会满足这种要求,它可能认为该函数过大或是存在递归操作(内联函数不能递归 ),因而不将其视为内联函数。
默认参数
C++
函数的默认参数 是指当函数调用中省略了实际参数时使用的一个默认值,这样可以极大提高函数使用的灵活性。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> using namespace std;int sum (int a = 2020 , int b = 1985 ) { return a + b; } int main (int argc, char *argv[]) { cout << "Total value is : " << sum () << endl; cout << "Total value is : " << sum (1 , 2 ) << endl; return 0 ; }
1 2 Total value is : 4005 Total value is : 3
函数重载
同一作用域当中,函数名称相同,参数个数 或者类型 不同的函数称为函数重戴 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> #include <string> using namespace std;string print (string parameter) { return "Hello " + parameter + "!" ; } string print (string parameter1, string parameter2) { return "Hello " + parameter1 + " and " + parameter2 + "!" ; } int main (int argc, char *argv[]) { cout << print ("Qt" ) << endl; cout << print ("Hank" , "Uinika" ) << endl; return 0 ; }
注意 :main()
函数不能进行重载。
泛型参数
通过参数泛型 来定义函数,泛型参数可以让编译器自动生成该类型参数对应的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> using namespace std;template <typename Type>void display (Type parameter) { cout << parameter << endl; } int main (int argc, char *argv[]) { display ("Qt" ); display (2020 ); return 0 ; }
异常处理
try
:侦测代码异常;
catch
:捕获并处理异常;
throw
:手动抛出异常;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> using namespace std;double division (int a, int b) { if ( b == 0 ) { throw "Zero cannot be used as a divisor!" ; } return (a/b); } int main (int argc, char *argv[]) { int x = 1985 ; int y = 0 ; int result; try { result = division (x, y); }catch (const char * message) { cerr << message << endl; } return 0 ; }
注意 :C++
并未提供finally
子句,而是采用类的析构函数来进行对象的销毁工作。
C++ 类
C++
以类 为核心实现了面向对象程序设计,而类本质上是一种抽象数据类型 (Abstract
Data
Type),包含了数据(成员变量 )以及数据的处理方法(成员函数 )。类的定义需要使用关键字class
,后面跟随类的名称,类的主体是包含在一对花括号{}
当中,最后以一个分号;
结尾。
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 26 27 28 29 30 31 32 33 34 #include <string> #include <iostream> using namespace std;class Person {public : string name; float height; float weight; void print () { cout<< name << endl << height << endl << weight <<endl; } }; int main (int argc, char *argv[]) { Person person1; person1. name = "Hank" ; person1. height = 182.5 ; person1. weight = 76.3 ; person1. print (); Person person2; person2. name = "Trump" ; person2. height = 190 ; person2. weight = 110 ; person2. print (); return 0 ; }
1 2 3 4 5 6 Hank 182.5 76.3 Trump 190 110
类的成员函数 即可以定义在类的内部 ,也可以借助范围解析运算符 ::
定义在类的外部 (但是函数声明需要放置在类内部):
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 26 27 #include <string> #include <iostream> using namespace std;class Person {public : string name; float height; float weight; void print () ; }; void Person::print () { cout<< name << endl << height << endl << weight <<endl; } int main (int argc, char *argv[]) { Person person; person.name = "Hank" ; person.height = 182.5 ; person.weight = 76.3 ; person.print (); return 0 ; }
访问修饰符
访问修饰符public
、private
、protected
用于约束各个类成员的访问限制,每个类可以使用多个访问修饰符,每个访问修饰符
作用于下一个访问修饰符出现之前,当类的成员没有修饰符时则默认为private
。
1 2 3 4 5 6 7 8 9 10 class Base { public : private : protected : };
public
public
成员在类的外部被访问,无需使用其它成员函数来设置set()
和获取get()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <string> #include <iostream> using namespace std;class Person {public : string name; void print () ; }; void Person::print () { cout<< name << endl; } int main (int argc, char *argv[]) { Person person; person.name = "Hank" ; person.print (); return 0 ; }
private
private
成员在类的外部不允许被访问,如果直接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <string> #include <iostream> using namespace std;class Person { private : string name; public : void print () ; }; void Person::print () { cout<< name << endl; } int main (int argc, char *argv[]) { Person person; person.name = "Hank" ; person.print (); return 0 ; }
1 2 3 C:\Workspace\test\main.cpp:20 : error: 'std::__cxx11::string Person::name' is private within this context person.name = "Hank" ; ^~~~
通常,需要借助于setter
和getter
方法来存取采用了private
修饰符的成员变量。
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 26 27 #include <string> #include <iostream> using namespace std;class Person { private : string name; public : string getName () ; void setName (string target) ; }; void Person::setName (string target) { name = target; } string Person::getName () { return name; } int main (int argc, char *argv[]) { Person person; person.setName ("Hank" ); cout << person.getName () << endl; return 0 ; }
protected
protected
成员与private
的特性类似,但是可以在其所在类的子类 当中进行访问。
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 26 27 28 29 30 31 #include <string> #include <iostream> using namespace std;class Person { protected : string name; }; class Hank :Person { public : string getName () ; void setName (string target) ; }; void Hank::setName (string target) { name = target; } string Hank::getName () { return name; } int main (int argc, char *argv[]) { Hank hank; hank.setName ("Hank" ); cout << hank.getName () << endl; return 0 ; }
构造函数
类的构造函数 会在每次创建类的新对象时被执行,可用于为成员变量赋初始值,构造函数名称与类名称完全相同,而且不会返回任何类型(包括void
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> using namespace std;class Hank {public : string name = "Hello C++ !" ; Hank () { cout<< name << endl; } }; int main () { Hank hank; return 0 ; }
构造函数同样可以采用范围解析运算符::
定义在类的外部,而且可以携带上参数,为类 的成员变量进行赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> using namespace std;class Hank {private : string name; public : Hank (string name); }; Hank::Hank (string name) { cout<<name<<endl; this ->name = name; } int main () { Hank hank ("Hello C++ !" ) ; return 0 ; }
注意 :构造函数访问修饰符即可以是public
,也可以是private
(只有该类的成员函数才能构造该对象)或者protected
(无法在类的外部构造该对象,只能构造该类的子类)。
析构函数
析构函数名称是在类名称前面添加了 1
个波浪号~
前缀,即没有任何返回值,也不能携带任何参数,通常用于关闭、清理、释放资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> using namespace std;class Hank {private : string name; public : Hank (){ cout<<"Constructor" <<endl; }; ~Hank (){ cout<<"Destructor" <<endl; }; }; int main () { Hank hank; return 0 ; }
析构函数同样可以通过范围解析运算符::
被定义在类的外部,改写上面的示例代码,将析构函数的定义移动到类的外部:
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 #include <iostream> using namespace std;class Hank {private : string name; public : Hank (){ cout<<"Constructor" <<endl; }; ~Hank (); }; Hank::~Hank (){ cout<<"Destructor" <<endl; }; int main () { Hank hank; return 0 ; }
this 指针
每个 C++
对象都能通过this
指针访问自身的十六进制地址,所有类的成员函数都可以通过引用this
指针来指向当前调用的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <string> #include <iostream> using namespace std;class Test {public : string name; void address () { cout<< this << endl; } }; int main (int argc, char *argv[]) { Test test; test.address (); cout<< &test << endl; return 0 ; }
注意 :友元函数没有this
指针,因为友元并非类的成员,仅有成员函数才拥有this
指针。
通过指针访问 C++
结构体 或者类 的成员,需要使用专门的成员访问运算符->
。下面代码中,无论是通过this
指针,还是通过*
声明的指针变量,访问成员变量和方法时,都需要采用->
运算符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <string> #include <iostream> using namespace std;class Test {public : string name; void address () { cout<< this ->name << endl; } }; int main (int argc, char *argv[]) { Test test; test.name = "Hello Qt!" ; Test *test_pointer = &test; test_pointer->address (); return 0 ; }
static 静态成员
C++
类当中声明为static
的称为静态成员,静态成员属于类,而非属于对象 。静态成员可以通过类名和范围解析运算符::
直接调用,而无需将类进行实例化。静态成员函数只能访问其它静态的成员变量和成员函数,并且不能够访问对象的this
指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <string> #include <iostream> using namespace std;class Demo {public : static string variable; static void function () { cout << variable << endl; } }; string Demo::variable = "Variable" ; int main (int argc, char *argv[]) { Demo::function (); return 0 ; }
通常情况下,类的静态成员不应该在类的内部进行初始化,但是可以为声明了constexpr
常量表达式类型的静态成员提供整型初始值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <string> #include <iostream> using namespace std;class Demo {public : static constexpr int variable = 32 ; }; int main (int argc, char *argv[]) { cout << Demo::variable << endl; return 0 ; }
friend 友元