C++简明教程

本教程旨在提取最精炼、实用的C++知识点,供读者快速学习及本人查阅复习所用,后期会持续更新。

基本语法

#include <iostream>
using namespace std;
 
// main() 是程序开始执行的地方
int main()
{
   cout << "Hello World" << endl; // 输出 Hello World
   return 0;
}
  • C++ 语言定义了一些头文件,这些头文件包含了程序中必需的或有用的信息。上面这段程序中,包含了头文件 <iostream>(include <>是只在include的目录下查找它的头文件,include ""表示在全盘查找它的头文件)。
  • using namespace std; 告诉编译器使用 std 命名空间。
  • int main() 是主函数,程序从这里开始执行。

数据类型

  • C++有7种基本的数据类型:


    基本数据类型

    可以使用signed,unsigned,short,long去修饰:


    类型大小
  • typedef声明
    可以使用 typedef 为一个已有的类型取一个新的名字。例如:
//typedef type newname; 
typedef int feet;
feet distance

变量

  • 变量定义
//type variable_name = value;
extern int d = 3, f = 5;    // d 和 f 的声明 
int d = 3, f = 5;           // 定义并初始化 d 和 f
byte z = 22;                // 定义并初始化 z
char x = 'x';               // 变量 x 的值为 'x'
  • 变量声明
    可以使用extern关键字在任意地方声明一个变量。
// 变量声明
extern int a, b;
extern float f;
  
int main ()
{
  // 变量定义
  int a, b;
  float f;

  return 0;
}

同样的,函数声明是,提供一个函数名即可,而函数的实际定义则可以在任何地方进行。

// 函数声明
int func();
 
int main()
{
    // 函数调用
    int i = func();
}
 
// 函数定义
int func()
{
    return 0;
}
  • 变量作用域
    1)在函数或一个代码块内部声明的变量,称为局部变量。
    2)在函数参数的定义中声明的变量,称为形式参数。
    3)在所有函数外部声明的变量,称为全局变量。

注:当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化为下列值:

变量初始化

  • 常量
    常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。


    转义字符常量
  • define预处理器
    下面是使用 #define 预处理器定义常量的形式:
#define LENGTH 10   
#define WIDTH  5
#define NEWLINE '\n'
  • const关键字
    可以使用 const 前缀声明指定类型的常量,如下所示:
const int  LENGTH = 10;
const int  WIDTH  = 5;
const char NEWLINE = '\n';

运算符

算数运算符

关系运算符

逻辑运算符

位运算符

赋值运算符

杂项运算符

运算符优先级

循环

一如常见的for,while,do while,可以用continue,break,goto来控制,不再赘述。

  • 无线循环
for( ; ; )
{
   printf("This loop will run forever.\n");
}

判断

if...else if...else,switch等常见套路。

  • switch的用法如下:
#include <iostream>
using namespace std;
 
int main ()
{
   // 局部变量声明
   char grade = 'D';
 
   switch(grade)
   {
   case 'A' :
      cout << "很棒!" << endl; 
      break;
   case 'B' :
   case 'C' :
      cout << "做得好" << endl;
      break;
   case 'D' :
      cout << "您通过了" << endl;
      break;
   case 'F' :
      cout << "最好再试一下" << endl;
      break;
   default :
      cout << "无效的成绩" << endl;
   }
   cout << "您的成绩是 " << grade << endl;
 
   return 0;
}
  • 条件运算符?:
Exp1 ? Exp2 : Exp3;

? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个 ? 表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个 ? 表达式的值。

函数

  • 函数定义
return_type function_name( parameter list )
{
   body of the function
}

// 示例:函数返回两个数中较大的那个数
int max(int num1, int num2) 
{
   // 局部变量声明
   int result;
 
   if (num1 > num2)
      result = num1;
   else
      result = num2;
 
   return result; 
}
  • 函数声明
return_type function_name( parameter list );
//示例
int max(int num1, int num2);
//在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:
int max(int, int);

注:当你在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。

  • 函数参数
    如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。
    当调用函数时,有多种向函数传递参数的方式:
    参数调用

    1)传值调用
    默认情况下,C++ 使用传值调用来传递参数。
    2)指针调用
    把参数的地址复制给形参
#include <iostream>
using namespace std;

// 函数定义
void swap(int *x, int *y)
{
   int temp;
   temp = *x;    /* 保存地址 x 的值 */
   *x = *y;        /* 把 y 赋值给 x */
   *y = temp;    /* 把 x 赋值给 y */
  
   return;
}

int main ()
{
   // 局部变量声明
   int a = 100;
   int b = 200;
 
   cout << "交换前,a 的值:" << a << endl;
   cout << "交换前,b 的值:" << b << endl;

   /* 调用函数来交换值
    * &a 表示指向 a 的指针,即变量 a 的地址 
    * &b 表示指向 b 的指针,即变量 b 的地址 
    */
   swap(&a, &b);

   cout << "交换后,a 的值:" << a << endl;
   cout << "交换后,b 的值:" << b << endl;
 
   return 0;
}

其中,&a、&b是指变量的地址,swap函数的形参*x、*y中的*是指从x、y的地址取值。(即实参为地址,形参通过指针引用)*
3)引用调用

#include <iostream>
using namespace std;

// 函数定义
void swap(int &x, int &y)
{
   int temp;
   temp = x; /* 保存地址 x 的值 */
   x = y;    /* 把 y 赋值给 x */
   y = temp; /* 把 x 赋值给 y  */
  
   return;
}

int main ()
{
   // 局部变量声明
   int a = 100;
   int b = 200;
 
   cout << "交换前,a 的值:" << a << endl;
   cout << "交换前,b 的值:" << b << endl;

   /* 调用函数来交换值 */
   swap(a, b);

   cout << "交换后,a 的值:" << a << endl;
   cout << "交换后,b 的值:" << b << endl;
 
   return 0;
}

实参为引用,形参通过加&引用实参(区别于传值引用)

  • 参数默认值
int sum(int a, int b=20)
{
  int result;
 
  result = a + b;
  
  return (result);
}

//调用的时候可以不传入b
sum(a);

数组

  • 数组声明
type arrayName [ arraySize ];
double balance[3] = {1000.0,20.0,30.0};
//如果省略掉了数组的大小,数组的大小则为初始化时元素的个数
double balance[] = {1000.0,20.0,30.0};
//数组元素可以通过数组名称加索引进行访问
double salary = balance[0];
  • 多维数组
type name[size1][size2]...[sizeN];
//例子
int threedim[5][10][4];

int a[3][4] = {  
 {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
 {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
 {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
};
int val = a[2][3];
  • 指向数组的指针
double *p;
double balance[10];

p = balance;

balance 是一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址。因此,上面的程序把 p 赋值为 balance 的第一个元素的地址,通过*p的方式即可访问到balance[0]的值。

  • 传递数组给函数
//方式1
void myFunction(int *param)
{
.
   int i = *param;
   int j = *(param + 1);
.
}
//方式2
void myFunction(int param[10])
{
.
.
.
}
//方式3
void myFunction(int param[])
{
.
   int i = param[0];
.
}
  • 从函数返回数组
    C++ 不允许返回一个完整的数组作为函数的参数。但是,可以通过指定不带索引的数组名来返回一个指向数组的指针。
#include <iostream>
#include <cstdlib>
#include <ctime>
 
using namespace std;
 
// 要生成和返回随机数的函数
int * getRandom( )
{
  static int  r[10];
 
  // 设置种子
  srand( (unsigned)time( NULL ) );
  for (int i = 0; i < 10; ++i)
  {
    r[i] = rand();
    cout << r[i] << endl;
  }
 
  return r;
}
 
// 要调用上面定义函数的主函数
int main ()
{
   // 一个指向整数的指针
   int *p;
 
   p = getRandom();
   for ( int i = 0; i < 10; i++ )
   {
       cout << "*(p + " << i << ") : ";
       cout << *(p + i) << endl;
   }
 
   return 0;
}

字符串

//C风格的字符串
char greeting[] = "Hello";
字符操作函数
//C++中的String类
string str1 = "Hello";

指针

  • 指针定义
    指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。指针变量声明的一般形式为:
type *var-name;
int    *ip;    /* 一个整型的指针 */

所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

  • 指针的使用
    使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。
#include <iostream>
 
using namespace std;
 
int main ()
{
   int  var = 20;   // 实际变量的声明
   int  *ip;        // 指针变量的声明
 
   ip = &var;       // 在指针变量中存储 var 的地址
 
   cout << "Value of var variable: ";
   cout << var << endl;
 
   // 输出在指针变量中存储的地址
   cout << "Address stored in ip variable: ";
   cout << ip << endl;
 
   // 访问指针中地址的值
   cout << "Value of *ip variable: ";
   cout << *ip << endl;
 
   return 0;
}

其结果为:

Value of var variable: 20
Address stored in ip variable: 0xbfc601ac
Value of *ip variable: 20

  • 常用指针操作
//空指针
int  *ptr = NULL;
cout << "ptr 的值是 " << ptr ;  //结果是:ptr 的值是 0

//指针递增
int  var[3] = {10, 100, 200};
ptr = var;   //数组的变量名代表指向第一个元素的指针
ptr++;

//指向指针的指针
int  var;
int  *ptr;
int  **pptr;

var = 3000;
// 获取 var 的地址    
ptr = &var;
// 使用运算符 & 获取 ptr 的地址
pptr = &ptr;

引用

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

  • 引用很容易与指针混淆,它们之间有三个主要的不同:
    1)不存在空引用。引用必须连接到一块合法的内存。
    2)一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
    3)引用必须在创建时被初始化。指针可以在任何时间被初始化。
// 声明简单的变量
int    i;
double d;
 
// 声明引用变量
int& r = i;
double& s = d;
  • 把引用作为返回值
    当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边。
#include <iostream>
 
using namespace std;
 
double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0};
 
double& setValues( int i )
{
  return vals[i];   // 返回第 i 个元素的引用
}
 
// 要调用上面定义函数的主函数
int main ()
{
 
   cout << "改变前的值" << endl;
   for ( int i = 0; i < 5; i++ )
   {
       cout << "vals[" << i << "] = ";
       cout << vals[i] << endl;
   }
 
   setValues(1) = 20.23; // 改变第 2 个元素
   setValues(3) = 70.8;  // 改变第 4 个元素
 
   cout << "改变后的值" << endl;
   for ( int i = 0; i < 5; i++ )
   {
       cout << "vals[" << i << "] = ";
       cout << vals[i] << endl;
   }
   return 0;
}

结果为:

改变前的值
vals[0] = 10.1
vals[1] = 12.6
vals[2] = 33.1
vals[3] = 24.1
vals[4] = 50
改变后的值
vals[0] = 10.1
vals[1] = 20.23
vals[2] = 33.1
vals[3] = 70.8
vals[4] = 50

当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。

int& func() {
   int q;
   //! return q; // 在编译时发生错误
   static int x;
   return x;     // 安全,x 在函数作用域外依然是有效的
}

日期和时间

C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用 <ctime> 头文件。

  • 时间相关的类型
    有四个与时间相关的类型:clock_t、time_t、size_t 和 tm。
    类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数。
    结构类型 tm 把日期和时间以 C 结构的形式保存,tm 结构的定义如下:
struct tm {
  int tm_sec;   // 秒,正常范围从 0 到 59,但允许至 61
  int tm_min;   // 分,范围从 0 到 59
  int tm_hour;  // 小时,范围从 0 到 23
  int tm_mday;  // 一月中的第几天,范围从 1 到 31
  int tm_mon;   // 月,范围从 0 到 11
  int tm_year;  // 自 1900 年起的年数
  int tm_wday;  // 一周中的第几天,范围从 0 到 6,从星期日算起
  int tm_yday;  // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起
  int tm_isdst; // 夏令时
}
  • 关于日期和时间的重要函数


    时间函数

基本的输入输出

  • I/O库头文件


    头文件
  • 标准输出流(cout)
cout << "Value of str is : " << str << endl;

其中,流插入运算符 << 在一个语句中可以多次使用,例如endl 用于在行末添加一个换行符。

  • 标准输入流(cin)
#include <iostream>
 
using namespace std;
 
int main( )
{
   char name[50];
 
   cout << "请输入您的名称: ";
   cin >> name;
   cout << "您的名称是: " << name << endl;
 
}

流提取运算符 >> 在一个语句中可以多次使用,如果要求输入多个数据,可以使用如下语句:

cin >> name >> age;
  • 标准错误流(cerr)
    cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。
cerr << "Error message : " << str << endl;
  • 标准日志流
    clog 对象是缓冲的。这意味着每个流插入到 clog 都会先存储在缓冲在,直到缓冲填满或者缓冲区刷新时才会输出。
clog << "Error message : " << str << endl;

编写和执行大型程序时,使用 cerr 流来显示错误消息,而其他的日志消息则使用 clog 流来输出。

数据结构

  • 定义结构
    struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book;

在结构定义的末尾,最后一个分号之前,可以指定一个或多个结构变量,这是可选的。上面是声明一个结构体类型 Books,变量为 book。

  • 访问结构成员
    为了访问结构的成员,我们使用成员访问运算符(.)
Books Book1;        // 定义结构体类型 Books 的变量 Book1
// Book1 详述
strcpy( Book1.title, "C++ 教程");
strcpy( Book1.author, "Runoob"); 
strcpy( Book1.subject, "编程语言");
Book1.book_id = 12345;
  • 结构作为函数参数
#include <iostream>
#include <cstring>
 
using namespace std;
void printBook( struct Books book );
 
// 声明一个结构体类型 Books 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
int main( )
{
   Books Book1;        // 定义结构体类型 Books 的变量 Book1
   Books Book2;        // 定义结构体类型 Books 的变量 Book2
 
    // Book1 详述
   strcpy( Book1.title, "C++ 教程");
   strcpy( Book1.author, "Runoob"); 
   strcpy( Book1.subject, "编程语言");
   Book1.book_id = 12345;
 
   // Book2 详述
   strcpy( Book2.title, "CSS 教程");
   strcpy( Book2.author, "Runoob");
   strcpy( Book2.subject, "前端技术");
   Book2.book_id = 12346;
 
   // 输出 Book1 信息
   printBook( Book1 );
 
   // 输出 Book2 信息
   printBook( Book2 );
 
   return 0;
}
void printBook( struct Books book )
{
   cout << "书标题 : " << book.title <<endl;
   cout << "书作者 : " << book.author <<endl;
   cout << "书类目 : " << book.subject <<endl;
   cout << "书 ID : " << book.book_id <<endl;
}
  • 指向结构的指针
    定义指向结构的指针,方式与定义指向其他类型变量的指针相似。为了使用指向该结构的指针访问结构的成员,必须使用 -> 运算符,如下所示:
struct Books *struct_pointer;
struct_pointer = &Book1;
struct_pointer->title;
  • typedef关键字
    typedef可以为创建的类型取一个"别名",例如:
typedef struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
}Books;
//现在即可直接使用 Books 来定义 Books 类型的变量
Books Book1, Book2;

typedef long int *pint32;
//x, y 和 z 都是指向长整型 long int 的指针。
pint32 x, y, z;

类和对象

  • 类的定义
class Box
{
   public:
      double length;   // 盒子的长度
      double breadth;  // 盒子的宽度
      double height;   // 盒子的高度
      double getVolume(void);// 返回体积
};
  • 类的声明
Box box1;
Box box2 = Box(parameters);
Box box3(parameters);
Box* box4 = new Box(parameters);
  • 访问类的成员
box1.length = 5.0;
cout << box1.length << endl;
  • 类成员函数
    成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义
class Box
{
   public:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
   
      double getVolume(void)
      {
         return length * breadth * height;
      }
};
//您也可以在类的外部使用范围解析运算符 :: 定义该函数
double Box::getVolume(void)
{
    return length * breadth * height;
}

//调用成员函数同样是在对象上使用点运算符(.)
Box myBox;          // 创建一个对象
myBox.getVolume();  // 调用该对象的成员函数
  • 类访问修饰符
    数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。
class Base {
 
   public:
 
  // 公有成员
 
   protected:
 
  // 受保护成员
 
   private:
 
  // 私有成员
 
};

1)公有成员在程序中类的外部是可访问的。
2)私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。如果没有使用任何访问修饰符,类的成员将被假定为私有成员
3)保护成员变量或函数与私有成员十分相似,但有一点不同,保护成员在派生类(即子类)中是可访问的。

  • 类的构造函数和析构函数
    1)类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。
    2)类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源
class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line();  // 这是构造函数
      Line(double len);  // 这是带参数的构造函数
      ~Line();  // 这是析构函数声明
 
   private:
      double length;
};
 
// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}

Line::Line( double len)
{
    cout << "Object is being created, length = " << len << endl;
    length = len;
}

Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
    delete ptr;
}
  • 拷贝构造函数
    拷贝构造函数是一种特殊的构造函数,通常用于:通过使用另一个同类型的对象来初始化新创建的对象。
class Line
{
   public:
      int getLength( void );
      Line( int len );             // 简单的构造函数
      Line( const Line &obj);      // 拷贝构造函数
      ~Line();                     // 析构函数
 
   private:
      int *ptr;
};

Line::Line(const Line &obj)
{
    cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
    ptr = new int;
    *ptr = *obj.ptr; // 拷贝值
}
  • 友元函数
    类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
class Box
{
   double width;
public:
   friend void printWidth( Box box );
   void setWidth( double wid );
};

// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
   /* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
   cout << "Width of box : " << box.width <<endl;
}
  • 内联函数
    当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。引入内联函数的目的是为了解决程序中函数调用的效率问题,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换 ,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的手段,所以内联函数一般都是10行以下的小函数,如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。
    内联函数注意点.png

    注:在类中定义的成员函数全部默认为内联函数,在类中声明,但在类体外定义的为普通函数。
  • this指针
    在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
class Box{
    public:
        Box(){;}
        ~Box(){;}
        Box* get_address()   //得到this的地址
        {
            return this;
        }
        double Volume()
        {
            return length * breadth * height;
        }
        int compare(Box box)
        {
            //指针通过->访问类成员,对象通过.访问类成员
            return this->Volume() > box.Volume();
        }
};

注:友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。

  • 指向类的指针
int main(void)
{
   Box Box1(3.3, 1.2, 1.5);    // Declare box1
   Box Box2(8.5, 6.0, 2.0);    // Declare box2
   Box *ptrBox;                // Declare pointer to a class.    
                               // 其中ptrBox为地址,*表示从其地址取值

   // 保存第一个对象的地址
   ptrBox = &Box1;

   // 现在尝试使用成员访问运算符来访问成员
   cout << "Volume of Box1: " << ptrBox->Volume() << endl;

   // 保存第二个对象的地址
   ptrBox = &Box2;

   // 现在尝试使用成员访问运算符来访问成员
   cout << "Volume of Box2: " << ptrBox->Volume() << endl;
  
   return 0;
}
  • 类的静态成员
    我们可以使用 static 关键字来把类成员定义为静态的。静态成员在类的所有对象中是共享的,当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
    注:如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化
class Box
{
   public:
      static int objectCount;
      // 构造函数定义
      Box(double l=2.0, double b=2.0, double h=2.0)
      {
         cout <<"Constructor called." << endl;
         length = l;
         breadth = b;
         height = h;
         // 每次创建对象时增加 1
         objectCount++;
      }
      double Volume()
      {
         return length * breadth * height;
      }
   private:
      double length;     // 长度
      double breadth;    // 宽度
      double height;     // 高度
};
 
// 初始化类 Box 的静态成员
int Box::objectCount = 1;

如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。


静态与普通成员函数的区别

继承

继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。类派生列表以一个或多个基类命名,形式如下:

class derived-class: access-specifier base-class

//例如
#include <iostream>
 
using namespace std;
 
// 基类 Shape
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 基类 PaintCost
class PaintCost 
{
   public:
      int getCost(int area)
      {
         return area * 70;
      }
};
 
// 派生类
class Rectangle: public Shape, public PaintCost
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect;
   int area;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   area = Rect.getArea();
   
   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;
 
   // 输出总花费
   cout << "Total paint cost: $" << Rect.getCost(area) << endl;
 
   return 0;
}

派生类可以访问基类中所有的非私有成员,同时,一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

重载运算符和重载函数

C++ 允许在同一个作用域内声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数。

调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。

#include <iostream>
using namespace std;
 
class printData
{
   public:
      void print(int i) {
        cout << "整数为: " << i << endl;
      }
 
      void print(double  f) {
        cout << "浮点数为: " << f << endl;
      }
 
      void print(char c[]) {
        cout << "字符串为: " << c << endl;
      }
};
 
int main(void)
{
   printData pd;
 
   // 输出整数
   pd.print(5);
   // 输出浮点数
   pd.print(500.263);
   // 输出字符串
   char c[] = "Hello C++";
   pd.print(c);
 
   return 0;
}
  • 重载运算符
    重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表,例如:
#include <iostream>
using namespace std;
 
class Box
{
   public:
 
      double getVolume(void)
      {
         return length * breadth * height;
      }
      void setLength( double len )
      {
          length = len;
      }
 
      void setBreadth( double bre )
      {
          breadth = bre;
      }
 
      void setHeight( double hei )
      {
          height = hei;
      }
      // 重载 + 运算符,用于把两个 Box 对象相加
      Box operator+(const Box& b)
      {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
      }
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};
// 程序的主函数
int main( )
{
   Box Box1;                // 声明 Box1,类型为 Box
   Box Box2;                // 声明 Box2,类型为 Box
   Box Box3;                // 声明 Box3,类型为 Box
   double volume = 0.0;     // 把体积存储在该变量中
 
   // Box1 详述
   Box1.setLength(6.0); 
   Box1.setBreadth(7.0); 
   Box1.setHeight(5.0);
 
   // Box2 详述
   Box2.setLength(12.0); 
   Box2.setBreadth(13.0); 
   Box2.setHeight(10.0);
 
   // Box1 的体积
   volume = Box1.getVolume();
   cout << "Volume of Box1 : " << volume <<endl;
 
   // Box2 的体积
   volume = Box2.getVolume();
   cout << "Volume of Box2 : " << volume <<endl;
 
   // 把两个对象相加,得到 Box3
   Box3 = Box1 + Box2;
 
   // Box3 的体积
   volume = Box3.getVolume();
   cout << "Volume of Box3 : " << volume <<endl;
 
   return 0;
}
  • 可重载与不可重载运算符


    可重载运算符

    不可重载运算符

多态

  • 虚函数
    虚函数是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
    若在基类中不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数,在函数参数后直接加 = 0 告诉编译器,函数没有主体,这种虚函数即是纯虚函数。
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
      
      // pure virtual function
      virtual int area() = 0;
};

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数,例如:

#include <iostream> 
using namespace std;
 
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};
class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }
};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }
};
// 程序的主函数
int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
 
   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();  //Rectangle class area
 
   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();  //Triangle class area
   
   return 0;
}

数据抽象与封装

  • 数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制,是一种依赖于接口实现分离的设计技术。
  • 数据封装是一种把数据和操作数据的函数捆绑在一起的机制
#include <iostream>
using namespace std;
 
class Adder{
   public:
      // 构造函数
      Adder(int i = 0)
      {
        total = i;
      }
      // 对外的接口
      void addNum(int number)
      {
          total += number;
      }
      // 对外的接口
      int getTotal()
      {
          return total;
      };
   private:
      // 对外隐藏的数据
      int total;
};
int main( )
{
   Adder a;
   
   a.addNum(10);
   a.addNum(20);
   a.addNum(30);
 
   cout << "Total " << a.getTotal() <<endl;
   return 0;
}

上面的类把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是用户不需要了解的,但又是类能正常工作所必需的。

  • 数据抽象的好处
    1)类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
    2)类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。
  • 设计策略
    通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

接口

接口描述了类的行为和功能,而不需要完成类的特定实现。如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。

示例可见虚函数一节

设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,如果没有在派生类中重载纯虚函数,就尝试实例化该类的对象,会导致编译错误。可用于实例化对象的类被称为具体类。

注:抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。

文件和流

如何从文件读取流和向文件写入流,这就需要用到 C++ 中另一个标准库 fstream,它定义了三个新的数据类型:

注:要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件 <iostream> 和 <fstream>。

  • 打开文件
void open(const char *filename, ios::openmode mode);

在这里,open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。

注:可以把以上两种或两种以上的模式结合使用。

//以写入模式打开文件,并希望截断文件,以防文件已存在
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
//打开一个文件用于读写
fstream  afile;
afile.open("file.dat", ios::out | ios::in );
  • 关闭文件
    当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。
    下面是 close() 函数的标准语法,close() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。
fstream  afile;
afile.open("file.dat", ios::out | ios::in );
afile.close();
  • 写入与读取文件
    在 C++ 编程中,我们使用流插入运算符( << )向文件写入信息,使用流提取运算符( >> )从文件读取信息。
#include <fstream>
#include <iostream>
using namespace std;
 
int main ()
{
   char data[100];
 
   // 以写模式打开文件
   ofstream outfile;
   outfile.open("afile.dat");
 
   cout << "Writing to the file" << endl;
   cout << "Enter your name: "; 
   cin.getline(data, 100);
   // 向文件写入用户输入的数据
   outfile << data << endl;
  
   cout << "Enter your age: "; 
   cin >> data;
   cin.ignore();
   // 再次向文件写入用户输入的数据
   outfile << data << endl;

   // 关闭打开的文件
   outfile.close();
 
   // 以读模式打开文件
   ifstream infile; 
   infile.open("afile.dat"); 
 
   cout << "Reading from the file" << endl; 
   infile >> data; 
   // 在屏幕上写入数据
   cout << data << endl; 
   
   // 再次从文件读取数据,并显示它
   infile >> data; 
   cout << data << endl; 
 
   // 关闭打开的文件
   infile.close();
 
   return 0;
}
  • 文件位置指针
    istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg("seek get")和关于 ostream 的 seekp("seek put")。
    seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
    文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。可通过以下实例掌握重点
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
 
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
 
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
 
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );

异常处理

C++ 异常处理涉及到三个关键字:try、catch、throw。

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。

  • catch: 在想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。

  • try: try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。它后面通常跟着一个或多个 catch 块。

  • 抛出异常
    可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。

double division(int a, int b)
{
   if( b == 0 )
   {
      throw "Division by zero condition!";
   }
   return (a/b);
}
  • 捕获异常
try
{
   // 保护代码
}catch( ExceptionName e1 )
{
   // catch 块
}catch( ExceptionName e2 )
{
   // catch 块
}catch( ExceptionName eN )
{
   // catch 块
}

上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...,例如:

try
{
   // 保护代码
}catch(...)
{
  // 能处理任何异常的代码
}
  • C++标准的异常
    C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。


  • 定义新的异常
    可以通过继承和重载 exception 类来定义新的异常。
#include <iostream>
#include <exception>
using namespace std;
 
struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "C++ Exception";
  }
};
 
int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}

动态内存

C++ 程序中的内存分为两个部分:

  • 栈:在函数内部声明的所有变量都将占用栈内存。
  • 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
new和delete运算符

在 C++ 中,可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。如果不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。

#include <iostream>
using namespace std;
 
int main ()
{
   double* pvalue  = NULL; // 初始化为 null 的指针
   pvalue  = new double;   // 为变量请求内存
 
   *pvalue = 29494.99;     // 在分配的地址存储值
   cout << "Value of pvalue : " << *pvalue << endl;
 
   delete pvalue;         // 释放内存
 
   return 0;
}
数组的动态内存分配

假设我们要为一个字符数组(一个有 20 个字符的字符串)分配内存,我们可以使用上面实例中的语法来为数组动态地分配内存:

char* pvalue  = NULL;   // 初始化为 null 的指针
pvalue  = new char[20]; // 为变量请求内存

要删除我们刚才创建的数组,语句如下:

delete [] pvalue;        // 删除 pvalue 所指向的数组

二维数组示例:

#include <iostream>
using namespace std;
 
int main()
{
    int **p;   
    int i,j;   //p[4][8] 
    //开始分配4行8列的二维数据   
    p = new int *[4];
    for(i=0;i<4;i++){
        p[i]=new int [8];
    }
 
    for(i=0; i<4; i++){
        for(j=0; j<8; j++){
            p[i][j] = j*i;
        }
    }   
    //打印数据   
    for(i=0; i<4; i++){
        for(j=0; j<8; j++)     
        {   
            if(j==0) cout<<endl;   
            cout<<p[i][j]<<"\t";   
        }
    }   
    //开始释放申请的堆   
    for(i=0; i<4; i++){
        delete [] p[i];   
    }
    delete [] p;   
    return 0;
}
对象的动态内存分配

对象与简单的数据类型没有什么不同:

#include <iostream>
using namespace std;
 
class Box
{
   public:
      Box() { 
         cout << "调用构造函数!" <<endl; 
      }
      ~Box() { 
         cout << "调用析构函数!" <<endl; 
      }
};
 
int main( )
{
   Box* myBoxArray = new Box[4];
 
   delete [] myBoxArray; // 删除数组
   return 0;
}

如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数。

命名空间

命名空间这个概念可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

定义命名空间

下面通过一个示例来展示如何定义命名空间并使用命名空间中的函数等。

#include <iostream>
using namespace std;
 
// 第一个命名空间
namespace first_space{
   void func(){
      cout << "Inside first_space" << endl;
   }
}
// 第二个命名空间
namespace second_space{
   void func(){
      cout << "Inside second_space" << endl;
   }
}
int main ()
{
 
   // 调用第一个命名空间中的函数
   first_space::func();
   
   // 调用第二个命名空间中的函数
   second_space::func(); 
 
   return 0;
}
using指令

可以使用 using namespace xxxx指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。

嵌套的命名空间

命名空间可以嵌套,可在一个命名空间中定义另一个命名空间,如下所示:

namespace namespace_name1 {
   // 代码声明
   namespace namespace_name2 {
      // 代码声明
   }
}
// 访问 namespace_name2 中的成员
using namespace namespace_name1::namespace_name2;
 
// 访问 namespace:name1 中的成员
using namespace namespace_name1;

模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

函数模板

模板函数定义的一般形式如下所示:

template <class type> 
ret-type func-name(parameter list)
{
   // 函数的主体
}

实例如下:

#include <iostream>
#include <string>
 
using namespace std;
 
//使用const&可节省传递时间,同时保证值不被改变
template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
   return 0;
}
类模板

泛型类声明的一般形式如下所示:

template <class type> 
class class-name {
     //类的主体
}

实例如下:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
 
using namespace std;
 
template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
 
template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 
 
template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 
 
template <class T>
T Stack<T>::top () const 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
    // 返回最后一个元素的副本 
    return elems.back();      
} 
 
int main() 
{ 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 
 
        // 操作 int 类型的栈 
        intStack.push(7); 
        cout << intStack.top() <<endl; 
 
        // 操作 string 类型的栈 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}

预处理器

预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。

#define预处理

#define 预处理指令用于创建符号常量。该符号常量通常称为宏,指令的一般形式是:

#define macro-name replacement-text 
//例如
#define PI 3.14159
参数宏

可以使用 #define 来定义一个带有参数的宏,如下所示:

#include <iostream>
using namespace std;
 
#define MIN(a,b) (a<b ? a : b)
 
int main ()
{
   int i, j;
   i = 100;
   j = 30;
   cout <<"较小的值为:" << MIN(i, j) << endl;
 
    return 0;
}
条件编译

有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。
条件预处理器的结构与 if 选择结构很像。请看下面这段预处理器的代码:

#ifndef NULL
   #define NULL 0
#endif

例如,要实现只在调试时进行编译,可以使用一个宏来实现,如下所示:

#ifdef DEBUG
   cerr <<"Variable x = " << x << endl;
#endif

使用 #if 0 语句可以注释掉程序的一部分,如下所示:

#if 0
   不进行编译的代码
#endif

下面给出一个示例:

#include <iostream>
using namespace std;
#define DEBUG
 
#define MIN(a,b) (((a)<(b)) ? a : b)
 
int main ()
{
   int i, j;
   i = 100;
   j = 30;
#ifdef DEBUG
   cerr <<"Trace: Inside main function" << endl;
#endif
 
#if 0
   /* 这是注释部分 */
   cout << MKSTR(HELLO C++) << endl;
#endif
 
   cout <<"The minimum is " << MIN(i, j) << endl;
 
#ifdef DEBUG
   cerr <<"Trace: Coming out of main function" << endl;
#endif
    return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Trace: Inside main function
The minimum is 30
Trace: Coming out of main function

#和##预处理运算符

# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。

#include <iostream>
using namespace std;
 
#define MKSTR( x ) #x
 
int main ()
{
    //转换成了cout << "HELLO C++" << endl;
    cout << MKSTR(HELLO C++) << endl;
 
    return 0;
}

## 运算符用于连接两个令牌。

#include <iostream>
using namespace std;
 
#define concat(a, b) a ## b
int main()
{
   int xy = 100;
   //转换成了cout << xy;
   cout << concat(x, y);
   return 0;
}
预定义宏

C++提供了下表所示的一些预定义宏:


多线程

多线程是多任务处理的一种特殊形式,一般情况下,有基于进程和基于线程的两种类型的多任务处理方式。

  • 基于进程的多任务处理是程序的并发执行。
  • 基于线程的多任务处理是同一程序的片段的并发执行。

C++11的标准库中提供了多线程库,使用时需要#include <thread>头文件,该头文件主要包含了对线程的管理类std::thread以及其他管理线程相关的类,下面通过一个示例简单了解线程的使用:

#include <iostream>
#include <thread>

using namespace std;

void output(int i)
{
    cout << i << endl;
}

int main()
{
    for (uint8_t i = 0; i < 4; i++)
    {
        //创建一个线程t,第一个参数为调用的函数,第二个参数为传递的参数
        thread t(output, i);
        //表示允许该线程在后台运行
        t.detach(); 
    }
    
    return 0;
}

在多线程并行的条件下,其输出结果不一定是顺序呢的输出1234,可能如下:


多线程并行
线程管理

每个应用程序至少有一个进程,而每个进程至少有一个主线程,除了主线程外,在一个进程中还可以创建多个子线程。每个线程都需要一个入口函数,入口函数返回退出,该线程也会退出,主线程就是以main函数作为入口函数的线程。

  • 启动线程
do_task();
std::thread(do_task);

//假设有一个函数,且函数名为output,则此处可创建一个线程执行该函数
thread t(output);

std::thread的构造函数需要的是可调用(callable)类型,除了函数外,还可以调用例如:lambda表达式、重载了()运算符的类的实例。

注:
1、把函数对象传入std::thread时,应传入函数名称(命名变量,如:output)而不加括号(临时变量,如:output())。
2、当启动一个线程后,一定要在该线程thread销毁前,调用t.join()或者t.detach(),确定以何种方式等待线程执行结束:

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
  • join方式,等待关联的线程完成,才会继续执行join()后的代码。

3、在以detach的方式执行线程时,要将线程访问的局部数据复制到线程的空间(使用按值传递),一定要确保线程没有使用局部变量的引用或者指针,除非你能肯定该线程会在局部作用域结束前执行结束。

  • 异常情况下等待线程完成
    为了避免主线程出现异常时将子线程终结,就要保证子线程在函数退出前完成,即在函数退出前调用join()。
    方法一:
void func() {
    thread t([]{
        cout << "hello C++ 11" << endl;
    });

    try
    {
        do_something_else();
    }
    catch (...)
    {
        t.join();
        throw;
    }
    t.join();
}

方法二:资源获取即初始化(RAII)

class thread_guard
{
    private:
        thread &t;
    public:
        /*加入explicit防止隐式转换,explicit仅可加在带一个参数的构造方法上,如:Demo test; test = 12.2;
        这样的调用就相当于把12.2隐式转换为Demo类型,加入explicit就禁止了这种转换。*/
        explicit thread_guard(thread& _t) {
            t = _t;
        }

        ~thread_guard()
        {
            if (t.joinable())
                t.join();
        }

        thread_guard(const thread_guard&) = delete;  //删除默认拷贝构造函数
        thread_guard& operator=(const thread_guard&) = delete;  //删除默认赋值运算符
};

void func(){

    thread t([]{
        cout << "Hello thread" <<endl ;
    });

    thread_guard guard(t);
}

无论是何种情况,当函数退出时,对象guard调用其析构函数销毁,从而能够保证join一定会被调用。

  • 线程互斥(std::mutex)
    通过mutex可以方便的对临界区域加锁,std::mutex类定义于mutex头文件,是用于保护共享数据避免从多个线程同时访问的同步原语,它提供了lock,try_lock,unlock等几个接口。使用方法如下:
std::mutex mtx;
mtx.lock()
do_something...;    //共享的数据
mtx.unlock();

mutex的lock和unlock必须成对调用,lock之后忘记调用unlock将是非常严重的错误,再次lock时会造成死锁。此时使用类模板std::lock_guard,通过RAII机制在其作用域内占有mutex,当程序流程离开创建lock_guard对象的作用域时,lock_guard对象被自动销毁并释放mutex。

std::mutex mtx;
std::lock_guard<std::mutex> guard(mtx);
do_something...;    //共享的数据
  • 向线程传递参数
    向线程调用的函数只需要在构造thread的实例时,依次传入即可。
  • 转移线程的所有权
    thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。
thread t1(f1);
thread t3(move(t1));

将线程从t1转移给t3,这时候t1就不再拥有线程的所有权,调用t1.join或t1.detach会出现异常,要使用t3来管理线程。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。

  • 线程标识的获取
    线程的标识类型为std::thread::id,有两种方式获得到线程的id:
    1)通过thread的实例调用get_id()直接获取
    2)在当前线程上调用this_thread::get_id()获取

C++ STL(标准模板库)

STL(Standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++程序库。STL中包括六大组件:容器、迭代器、算法、仿函数、迭代适配器、空间配置器。

容器

STL中的常用容器包括:序列式容器(vector、deque、list)、关联式容器(map、set)、容器适配器(queue、stack)。
1)序列式容器

  • vector
    vector是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问。由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。其常用操作如下:
//需要包含头文件
#include <vector>

//1.定义和初始化
vector<int> vec1;    //默认初始化,vec1为空
vector<int> vec2(vec1);  //使用vec1初始化vec2
vector<int> vec3(vec1.begin(),vec1.end());//使用vec1初始化vec2
vector<int> vec4(10);    //10个值为0的元素
vector<int> vec5(10,4);  //10个值为4的元素

//2.常用操作方法
//2.1 添加函数
vec1.push_back(100);            //尾部添加元素
vec1.insert(vec1.end(),5,3);    //从vec1.back位置插入5个值为3的元素

//2.2 删除函数
vec1.pop_back();              //删除末尾元素
vec1.erase(vec1.begin(),vec1.begin()+2);  //删除vec1[0]-vec1[2]之间的元素,不包括vec1[2]其他元素前移
vec1.clear();                 //清空元素,元素在内存中并未消失,通常使用swap()来清空
vector<int>().swap(V);        //利用swap函数和临时对象交换内存,交换以后,临时对象消失,释放内存。

//2.3 遍历函数
vec1[0];        //取得第一个元素
vec1.at(int pos);     //返回pos位置元素的引用
vec1.front();  //返回首元素的引用
vec1.back();  //返回尾元素的引用
vector<int>::iterator begin= vec1.begin();  //返回向量头指针,指向第一个元素
vector<int>::iterator end= vec1.end();  //返回向量尾指针,指向向量最后一个元素的下一个位置
vector<int>::iterator rbegin= vec1.rbegin();  //反向迭代器,指向最后一个元素
vector<int>::iterator rend= vec1.rend();  //反向迭代器,指向第一个元素之前的位置

//2.4 判断函数
bool isEmpty = vec1.empty();    //判断是否为空

//2.5 大小函数
int size = vec1.size();  //元素个数
vec1.capacity();  //返回容器当前能够容纳的元素个数
vec1.max_size();  //返回容器最大的可能存储的元素个数

//2.6 改动函数
vec1.assign(int n,const T& x);  //赋n个值为x的元素到vec1中,这会清除掉vec1中以前的内容。
vec1.assign(const_iterator first,const_iterator last);  //当前向量中[first,last)中元素设置成迭代器所指向量的元素,这会清除掉vec1中以前的内容。
  • deque
    所谓的deque是”double ended queue”的缩写,双向队列不论在尾部或头部插入元素,都十分迅速。而在中间插入元素则会比较费时,因为必须移动中间其他的元素。
#include <deque>  // 头文件

//1.声明和初始化
deque<type> deq;  // 声明一个元素类型为type的双端队列que
deque<type> deq(size);  // 声明一个类型为type、含有size个默认值初始化元素的的双端队列que
deque<type> deq(size, value);  // 声明一个元素类型为type、含有size个value元素的双端队列que
deque<type> deq(mydeque);  // deq是mydeque的一个副本
deque<type> deq(first, last);  // 使用迭代器first、last范围内的元素初始化deq

//2.常用成员函数
deq[index];  //用来访问双向队列中单个的元素。
deq.at(index);  //用来访问双向队列中单个的元素。
deq.front();  //返回第一个元素的引用。
deq.back();  //返回最后一个元素的引用。
deq.push_front(x);  //把元素x插入到双向队列的头部。
deq.pop_front();  //弹出双向队列的第一个元素。
deq.push_back(x);  //把元素x插入到双向队列的尾部。
deq.pop_back();  //弹出双向队列的最后一个元素。
  • list
    list是STL实现的双向链表,与vector相比, 它允许快速的插入和删除,但是随机访问却比较慢。
#include <list>

//1.定义和初始化
list<int>lst1;          //创建空list
list<int> lst2(5);       //创建含有5个元素的list
list<int>lst3(3,2);  //创建含有3个元素值为2的list
list<int>lst4(lst2);    //使用lst2初始化lst4
list<int>lst5(lst2.begin(),lst2.end());  //同lst4

//2.常用操作函数
lst1.assign(lst2.begin(),lst2.end());  //给list赋值为lst2
lst1.back(); //返回最后一个元素 
lst1.begin();  //返回指向第一个元素的迭代器 
lst1.clear();  //删除所有元素 
lst1.empty();  //如果list是空的则返回true 
lst1.end();  //返回末尾的迭代器 
lst1.erase();  //删除一个元素 
lst1.front();  //返回第一个元素 
lst1.insert();  //插入一个元素到list中 
lst1.max_size();  //返回list能容纳的最大元素数量 
lst1.merge();  //合并两个list 
lst1.pop_back();  //删除最后一个元素 
lst1.pop_front();  //删除第一个元素 
lst1.push_back();  //在list的末尾添加一个元素 
lst1.push_front();  //在list的头部添加一个元素 
lst1.rbegin();  //返回指向第一个元素的逆向迭代器 
lst1.remove();  //从list删除元素 
lst1.remove_if();  //按指定条件删除元素 
lst1.rend();  //指向list末尾的逆向迭代器 
lst1.resize();  //改变list的大小 
lst1.reverse();  //把list的元素倒转 
lst1.size();  //返回list中的元素个数 
lst1.sort();  //给list排序 
lst1.splice();  //合并两个list 
lst1.swap();  //交换两个list 
lst1.unique();  //删除list中相邻重复的元素
  • map
    map是STL的一个关联容器,它是一种键值对容器,里面的数据都是成对出现的,可在我们处理一对一数据的时候,在编程上提供快速通道。map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的。
#include <map>
 
//1.定义与初始化
map<int, string> ID_Name;
// 使用{}赋值是从c++11开始的,因此编译器版本过低时会报错,如visual studio 2012
map<int, string> ID_Name = {{ 2015, "Jim" },{ 2016, "Tom" },{ 2017, "Bob"}};

//2.基本操作函数
count()         //返回指定元素出现的次数
find()          //查找一个元素
get_allocator() //返回map的配置器
key_comp()      //返回比较元素key的函数
lower_bound()   //返回键值>=给定元素的第一个位置
upper_bound()    //返回键值>给定元素的第一个位置
value_comp()     //返回比较元素value的函数
map<int,string>::iterator iter_map = map1.begin();//取得迭代器首地址
int key = iter_map->first;             //取得key
string value = iter_map->second;       //取得value
  • set
    set的含义是集合,它是一个有序的容器,里面的元素都是排序好的支持插入、删除、查找等操作,就像一个集合一样,所有的操作都是严格在logn时间内完成,效率非常高,使用方法类似list。

以上,若有错误烦请指出,有地方不理解欢迎讨论。
github传送地址:https://github.com/JunJieDing666

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容