C++教程
头文件
c中的标准头文件是#include <stdio.h>,c++中不需要
//hello world
// iostream 输入输出流,才能使用cin,cout
#include <iostream>
// 没有下面这行的话输出时应该: std::cout << "asd";
using namespace std;
int main(){
cout << "hello world" << endl;
//让程序暂停一下,按任意键继续
system("pause");
return 0;
}
头文件cstring、string、string.h的区别
cstring:只支持MFC的工程项目
string:是c++标准的类库,也是STL里的库
string.h:其中.h的头文件是c语言的头文件,这里是c语言对于字符数组的函数定义的头文件
所以,c++中若要用cout输出string类型的字符串,需要用头文件
#include <string>
typedef声明
//使用typedef为一个已有的数据类型取个新名字,不能定义变量
//用法typedef type newname
typedef int feet
枚举类型
c++的一种派生数据,他是由用户定义的若干枚举常量的集合;枚举元素是一个整形,枚举型可以隐式转换为int,int不可隐式转换为枚举型。
enum name{
标识符 [=整型常数],
标识符 [=整型常数]
}枚举变量;
如果枚举没有初始化,即省掉"=整型常数"时,则从第一个标识符开始,第一个名称的值为0,第二个名称的值为1,以此类推.但是可以给名称赋予一个特殊的值,只需要添加一个初始值即可.
enum course {math,chinese,english,physics,chemistry}c;
c = english;
int a = chinese;
cout<<c<<a<<endl; //21
//english为6 physics为7 chemistry为8,chinese仍为1,math仍为0
enum cur {math,chinese,english=6,physics,chemistry}c;
c = english;
int a = chinese;
cout<< c << a << physics <<endl; //617
变量的声明和定义
//变量的声明不分配内存 extern 数据类型 变量名;声明之后也必须定义才能用这些变量
extern int a,b;
int mian()
{
//定义
int a,b;
return 0;
}
运算符
-
算术运算符:+ - * / % ++ –
-
关系运算符:== != < > >= <=
-
逻辑运算符:&& || !
-
位运算符:& | ^ ~ << >>
-
赋值运算符:= += -= *= /= %= <<= >>= &= ^= !=
-
杂项运算符:
sizeof //返回变量的大小,eg:sizeof(a)返回4 a是整型 sizeof(int) Condition?X:Y //三元运算符 Condition为true,值为X,否则值为Y , //逗号表达式,值为最后一个表达式的值 .和-> //用于引用类、结构和公用体的成员 Cast //强制类型转换符 eg:int(2.202)返回2 & //指针运算符 返回变量的地址 * //指针运算符 指向一个变量
-
运算符优先级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uZNAxe4F-1657612698904)(C:\Users\24091\Desktop\笔记\image-20220706140613732.png)]
循环
int array[5] = { 11, 22, 33, 44, 55 };
for (int x : array)
{
cout << x << " ";
}
cout << endl;
// auto 类型也是 C++11 新标准中的,用来自动获取变量的类型
for (auto x : array)
{
cout << x << " ";
}
//仿函数for_each,for_each用于逐个遍历容器元素,它对迭代器区间[first,last)所指的每一个元素,执行由单参数函数对象f所定义的操作。
//使用时需要加头文件#include <algorithm>
for_each(&array[0],&array[5],myfunc);
预处理
-
宏定义: #define 标识符 字符串
-
文件包括: #include 或者 #include “filename”
-
条件编译
//如果标识符被#define定义过,执行程序段1,否则执行程序段2 #ifdef 标识符 程序段1 #else 程序段2 #endif //如果标识符没有被#define定义过,执行程序段1,否则执行程序段2 #ifndef 标识符 程序段1 #else 程序段2 #endif //如果表达式为true,执行程序段1,否则执行程序段2 #if 表达式 程序段1 #else 程序段2 #endif
数组
定义一维数组的形式:数据类型 数据名[int常量表达式]
初始化的形式:数据类型 数组名[int常量表达式] = {初值表};
为数组的某一个元素赋值:数组名[下标] =值
(下标从0开始)
数组的引用:数组名[下标]
- 初始化数组时,可以只给部分元素赋值
- 对全部元素数组赋值时,可以不指定数组长度,编译系统会根据初值个数确定数组的长度。
- static型数组元素不赋初值,系统会自动默认为0。
int arr1[4] = {1,2,3,4};
int arr2[4] = { 1,2 };
int arr[4] = {0};//所有元素为0
static int arr3[3];
int arr4[4];
cout << "arr:"<<arr[0] << arr[1] << arr[2] << arr[3] << endl;
cout << "arr1:"<<arr1[0] << arr1[1] << arr1[2] << arr1[3] << endl;
cout << "arr2:" << arr2[0] << arr2[1] << arr2[2] << arr2[3] << endl;
cout << "arr3:" << arr3[0] << arr3[1] << arr3[2] << arr3[3] << endl;
cout << "arr4:" << arr4[0] << arr4[1] << arr4[2] << arr4[3] << endl;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2R6njnCQ-1657612698906)(C:\Users\24091\AppData\Roaming\Typora\typora-user-images\image-20220706143743970.png)]
字符数组:char类型的数组,在支付数组中最后一位为’\0’
//字符串长度为8个字节,最后一位是'\0'。
char array[10] = "yuanrui";//yuanrui\0\0\0
//也可以不用定义字符串长度,如:
char arr[] = "yuanrui";//yuanrui\0
指向数组的指针
int *p;
int arr[5]={1,4,3,7,5};
p = arr; //p = &arr[0];
cout <<*p<<","<<*(p+3);//1,7
动态创建数组
int *ptr = new int[n];//创建一维动态数组,但未初始化。
int *pt2 = new int[n]();//()代表初始化为0,注意中间不能填值。
int *pt3 = new int[n]{8};//用{}初始化,只有pt3[0]为8。而且用时比()多。
int *pt4 = new int[n];
// memset函数包含于string.h中
memset(pt4,8,n*sizeof(int));//pt4的数组全为8,速度和使用()相当。
//当然我也可以用循环给数组赋值,但是比memset慢。
delete []ptr;
//二维数组也类似
int **ptr;
*ptr = new int*[n];
for(int i = 0; i < n; ++i)
ptr[i] = new int[m](); //初始化
...
for(int i = 0; i < n; ++i)//删除动态数组
delete[] ptr[i];
delete[] ptr;
数组与函数
-
传递二位数组
如果传递二维数组,形参必须制定第二维的长度。
形式参数是一个指针:void function(int *param)
形式参数是一个已定义大小的数组:void function(int param[10])
形式参数是一个未定义大小的数组:void function(int param[])
二维数组:void function(int a[][3],int size) -
函数返回数组
C++不支持在函数外返回局部变量的地址,除非定义局部变量为static.
int * func(); int ** func();
获取数组大小
- 动态创建的基本数据类型数组无法取得数组大小
int a[3];
//第一种方法
cout<<sizeof(a)/sizeof(a[0])<<endl;
//第二种方法
cout << end(a) - begin(a) << endl;
//二维数组
int arr[5][3];
int lines = sizeof(arr) / sizeof(arr[0][0]);
int row = sizeof(arr) / sizeof(arr[0]);//行
int col = lines / row;//列
cout << row << "::"<<col << endl;
cout << end(arr) - begin(arr) << endl;//5行
函数
函数可以重载
递归
- 一个函数直接或者间接递归调用该函数本身,成为函数的递归调用
- 递归和回归:原问题->子问题 子问题的解->原问题的解
内联(inline)函数
c++在编译时可以将调用的函数代码嵌入到主调函数中,这种嵌入到主调函数中的函数称为内联函数,又称为内嵌函数或内置函数。
- 定义内联函数时,在函数定义和函数原型声明时都使用inline,也可以只在其中一处使用,效果一样.
- 内联函数在编译时用内联函数的函数体替换,所以不发生函数调用,不需要保护现场,恢复现场,节省了开销.
- 内敛函数增加的目标程序的代码量.因此,一般只将函数规模很小且使用频繁的函数声明为内联函数.
- 当内联函数中实现过于复杂时,编译器会将它作为一个普通函数处理,所以内联函数内不能包含循环语句和switch语句.
inline 函数类型 函数名(形参){
函数体;
}
inline int add(int a,int b){
return a+b;
}
字符串
-
c语言中的字符串(字符数组):char a[5].
-
c++中的字符串(string)
string str1; string str2 = "shanghai"; string str3 = str2; str[3] = '2'; //字符串数组 string arr[3]={"yi","er","san"}; for(auto s : arr){ cout <<s<<endl; } //访问string每个字符串 string arr="zifuchuan"; for(auto s : arr) cout <<s; cout <<endl; //使用迭代器逆置输出 for (string::iterator iter = arr.end();iter >=arr.begin();iter--) cout << *iter; //反向迭代器 for(string::reverse_iterator riter = arr.rbegin(); ; riter < arr.rend() ; riter++) cout<<*riter;
指针
- &取地址符,&变量名表示变量的地址
- *指针运算符(间接运算符,间接取值 ), 其后的变量为指针变量,表示该指针变量所指向的变量。指针变量的值通常为另一个变量的地址(即变量在内存中的直接地址)。
- 变量的数据类型必须和指针变量的数据类型一致
- 安全起见,指针初始化时为空指针(NULL或0)
- & * 的优先级是相同的,结合方式是至左向右。
//初始化
int a;
int *p = &a;
int *p2;
p2 = &a;
int var1,var2;
cout << "var1的地址:"<<&var1<<endl;
cout << "var2的地址:"<<&var2<<endl;
//指针的使用示例
int var = 20;
int *ip;
//将var在内存中的地址存储到ip这个变量中
ip = &var;
cout << "var的值:" << var<<endl;
cout << "var的内存地址:"<<ip<<endl;
cout << "使用指针访问var的值"<< *ip << endl;
指针与数组
-
数组名是数组的首地址,eg:arr为arr[0]的地址.
-
访问数组元素:arr[i],(arr+i),(p+i),p[i]
-
二维数组:arr+i == &arr[i],arr[i] == &arr[i][0] ,*(arr[i]+j) == arr[i][j]
-
指针访问二维数组:指向二维数组元素,指向一维数组
-
数组指针:数据类型 (*指针变量名) [m]
int arr[10]; int *p1 = arr;// *p1 = &arr[0]; int a[3][5] = { 0 }; int(*ap)[5]; ap = a; ap+1;//表示下一个一维数组
指针与字符串
- 字符串数字名 char ch[] = “man”; char *p = ch;
- 字符串: char *p = “man”;
- 指针赋值运算: char *p; p = “man”;
指针与函数
指针可以作为函数的参数,也可以作为函数的返回值.
引用
引用可以看作是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据,类似与快捷方式
- 引用不占内存空间,必须在定义的同时初始化,且不能再引用其他数据.
- 引用在定义时需要添加&,在使用时不能添加&,使用时添加&表示取地址
引用型变量声明, 数据类型 &引用名 = 变量名;
int a;
int &b = a; //a和b表示相同的变量,具有相同的地址
引用可以作为函数参数,也可以作为函数返回值
void swap(int &r1, int &r2){
int temp = r1;
r1 = r2;
r2 = temp;
}
int &add(int &r){
r+=1;
return r;
}
void add(int &r){
r+=1;
}
int main()
{
int r1 = 1,r2 = 2;
swap(r1,r2); // r1:2 , r2:1;
int &b = add(r1); // r1:3 , b:3;
cout<< r1 <<","<<r2<<","<<b;
return 0;
}
==将引用作为函数返回值时不能返回局部数据的引用,因为当函数调用完成后局部数据会被销毁。==函数在栈上运行,函数调用结束之后,后面的函数调用会夫嘎斯之前函数的局部数据。
int &add1(int &r){
r +=1;
int res =r;
return res;
//semantic issue
//warning: reference to stack memory associate with local variable 'res' returned
//会警告,返回局部变量'res'
}
int main()
{
int a=12;
int &b = add1(a);
int &c = add1(a);
cout << a << ","<<b<<","<<c<<endl;//输出:14, 之后的程序都不会执行.
return 0;
}
new和delete运算符
- new为变量分配内存空间
- 可以通过判断new返回的指针的值,判断空间是否分配成功
- delete释放空间
指针变量 = new 数据类型(初值);
delete 指针变量;
delete[] 指针变量;//释放为多个变量分配的地址
int *ip;
ip = new int(1);
delete ip;
int* ip;
ip = new int[10];
for(int i=0;i<10;i++){
ip[i]=i;
}
delete[] ip;
string* str = new string("mmm");
cout << *str;
const关键字
const是英文“constant”的缩写,意为“常量,常数”,用const关键字修饰的变量被成为常变量,即变量值不可以改变的只读变量。
常量的本质上仍是一个变量,有别于字面常量,两者最大的区别是:通常情况下,常变量经编译后需要占用一定的内存空间,是可以寻址的;而字面常量经编译后其值存储在代码区,不可寻址。但是两者间没有绝对的界限,某些编译对代码进行编译优化时,会将常变量转化为字面变量,变得不可寻址。
- const可以用来修饰变量的值,也可以用来修饰指针。
- const用来修饰变量的值时,变量的值不可变,这样的const被称为 底层const;const用来袖子指针时,指向变量的指针的值不可改变(),这样的const被成为顶层const。通常将指向常变量的指针成为常量指针,将值不可以改变的指针称为指针常量。
- 如果const出现在*左边,表示被指物是常量;如果const出现在*右边,表示指针自身是常量;如果两边都有,表示两者都是常量。
const int i=1;
//底层const, 常量指针
const int *p1 = &i;
int const *p2 = &i;
//顶层const,p是指针常量
int *const p = &i;
//对常变量的引用也必须是常变量类型的引用
const int i = 1;
const int &r = i; //正确
int &r2 = i; //编译错误
一些字符串处理函数的字符串参数都声明为const类型的原因
- 这样可以避免无意间修改数据而导致错误
- 使用const使得函数能同时处理const和非const的数据,否则只能接收非const数据
- const引用使得函数可以正确生成并使用临时变量(非常量引用的初始值必须为左值)
int refcube(const int &ra)//此处去掉const会无法编译
{
return ra * ra *ra;
}
int main()
{
int i = 2;
cout << refcube(i + 1) <<"是" << i + 1<< "的立方" << endl;
}
上例中:i+1不是左值,因为c++中不允许存在i+1=5
这样的语句(这句话不是i+1等于5,而是将5赋值给i+1),所以要使用i+1作为函数参数,必须声明为const引用类型。这时编译器会生成一个临时匿名变量,并让ra成为它的别名,临时变量只在函数调用期间存在,此后编译器会随意将其删除。
自定义数据类型
结构体
//结构体可以包含不同数据类型的结构
struct 结构体类型名{
成员类型1 成员名1;
成员类型2 成员名2;
...
};
//定义结构体同时声明结构体变量名
struct 结构体类型名{
成员类型1 成员名1;
成员类型2 成员名2;
...
}变量名1,变量名2...;
结构体变量的使用:
- 具有相同类型的结构体变量可以进行赋值运算,但是不能输入输出
- 对结构体变量的成员引用:结构体变量名.成员名
- 指向结构体的指针变量引用格式:指针变量名->成员名;
- 结构体数组的定义,初始化和使用与结构体变量、基本类型数组相似
struct person
{
int year;
int age;
string name;
}p[2] ={ {2019,24,"heiren"}, { 2020,24,"heiren" }};//可以不指定数组元素个数
p[1].age;
公用体(union)
几个不同的变量共享同一个地址开始的内存空间
- 成员类型可以是基本数据类型,也可以是构造数据类型。
- 公用体变量初始化时,只能对第一个成员赋值。
- 公用体变量所占的内存长度等于最长的成员长度。
- 公用体变量在一个时刻只能一个成员发挥作用,赋值时,成员之间会互相覆盖,最后一次被赋值的成员起作用。
//初始化
union data{
int i;
float f;
char c;
}x={123};//定义结构体的同时定义变量名,在其他地方可以直接使用x变量.
面向对象
类
类是一中抽象的数据类型,对象是类的实例(也就是说类进行实例化之后便成为对象)
- C++中public、private、protected只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分
- 类内部没有访问权限的限制,都可以互相访问。
- 在C++中用class定义的类中,其成员的默认存取权限是private。
class 类名{
public:
公有数据成员;
公有成员函数;
private:
私有数据成员;
私有成员函数;
protected:
保护数据成员;
保护成员函数;
}
//类外
返回类型 类名:成员函数名(参数列表)
{
函数体;
}
//内联函数:类外
inline 返回类型 类名:成员函数名(参数列表)
{
函数体;
}
类和对象之间的联系
- 一个类对象可能是另一个类的成员(对象成员)
- 一个类的成员函数是另一个类的友元成员
- 一个类定义在另一个类的说明中,即类嵌套
- 一个类作为另一个类的派生类
构造函数
构造函数是一种特殊的成员函数(可以重载),主要功能是为对象分配存储空间,以及为类成员变量赋初值
- 构造函数名必须与类名相同
- 没有任何返回值和返回类型
- 创建对象时自动调用,且只有一次
- 如果类没有定义任何构造函数,系统会为这个类生成一个默认无参是构造函数
//1.类中定义 2.类中声明 3. 类外定义
类名::构造函数名(参数列表){
函数体
}
class Person{
public:
//类中定义构造函数
Person(string str){
age = 10;
name = str;
}
Person(int a= 10,string str= "man");
void show();
private:
int age;
string name;
};
//类外定义,(重载)
Person::Person(int a,string str){
age = a;
name = str;
}
// 简写构造函数
// 原构造函数的方式
class Time
{
public:
//以下两种方式等价
Time(int h,int m,int s):hour(h),minute(m),second(s) {}
Time(int h,int m,int s){
hour = h;
minute = m;
second = s;
}
int hour;
int minute;
int sec;
};
拷贝构造函数
拷贝构造函数首先是构造函数,和类同名,且没有返回类型。
其次,它只有一个参数,类型为该类类型的引用,且通常为常量引用(const)
- 作用:用一个对象初始化另一个对象。复制对象,并将它作为实参传给函数。复制对象,并将它从函数返回。初始化序列容器的对象。用元素初始化值列表初始化数组中的元素。
class Person{
public:
//声明拷贝构造函数
Person(const Person &p);
//定义构造函数
Person(string str="man"){
age = 10;
name = str;
cout<<"gouzao"<<name<<endl;
}
private:
int age;
string name;
};
//定义拷贝构造函数
Person::Person(const Person &p){
age = p.age;
name = p.name;
cout << "copy gouzao"<<name<<endl;
}
int main()
{
Person p;
Person p1(p);
return 0;
}
析构函数
是一种特殊的成员函数,当对象的生命周期结束时,用释放分配给对象的内存空间,并做一些清理工作
- 析构函数名与类名必须相同。
- 析构函数名前面必须加一个波浪号~。
- 没有参数,没有返回值,不能重载。
- 一个类中只能有一个析构函数。
- 没有定义析构函数,编译系统会自动为和这个类生成一个默认的析构函数。
//类中定义
~Person(){
函数体
}
//类中声明,外定义
Person::~Person(){
函数体
}
对象指针
对象成员
自定义类的数据成员是另一个类的对象(两个角色)
例如
- 类B的对象是类A的一个成员,则该成员就成为类A的对象成员
- 这意味着一个类A的“大对象”包含着一个B类的“小对象”
- 也就是说,类B对象属于类A对象
- 在类中声明对象成员并不会创建该对象(类外声明对象表明创建了一个对象,二者有所不同)
对象有地址,存放对象初始地址的指针变量就是指向对象的指针变量。对象中的成员也有地址,存放对象成员地址的指针变量就是指向对象成员的指针变量。
对象指针的声明和使用
Person p(123,"man");
Person *p1 = &p;
Person *p2 = new Person(111,"mmm");
指向对象成员的指针
Time t(11,2,33);
t.show();
int *p=&t.hour;
cout <<*p<<endl;
Time *p1 = new Time(11,22,33);
//定义指针变量p2,并指向t
Time *p2 = &t;
p2->show();
//定义一个指向time类的公共函数的指针变量p3
void (Time::*p3)();
p3 = &Time::show;
//调用对象t1中p3所指的成员函数,与t.show()同作用
(t.*p3)();
this指针
每个成员函数都有一个特殊的指针this,它始终指向当前被调用的成员函数操作的对象
class Person{
public:
//定义构造函数
Person(int a=11){
age = a;
name = "str";
}
void show(){
cout << "age"<< this->age << endl; //11
cout << "age" << age << endl; //11
}
private:
int age;
string name;
};
静态成员
以关键字static开头的成员为静态成员,多个类共享。
-
static成员变量属于类,不属于某个具体的对象
-
静态成员函数只能访问类中静态数据成员
//静态数据成员 class xxx{ static 数据类型 静态数据成员名; } 数据类型 类名::静态数据成员名=初值 //访问 类名::静态数据成员名; 对象名.静态数据成员名; 对象指针名->静态数据成员名; //静态成员函数 //类内声明,类外定义 class xxx{ static 返回值类型 静态成员函数名(参数列表); } 返回值类型 类名::静态成员函数名(参数列表){ 函数体; } //访问 类名::静态成员函数名(参数列表); 对象名.静态成员函数名(参数列表); 对象指针名->静态成员函数名(参数列表);
友元(friend)
借助友元,可以使得其他类中的成员函数以及全局范围内的函数访问当前的private成员.
友元函数
- 友元函数不是类的成员函数,所以没有this指针,必须通过参数传递对象.
- 友元函数中不能直接引用对象成员的名字,只能通过参数传递进来的对象或对象指针来引用该对象的成员.
//1. 将非成员函数声明为友元函数
class Person{
public:
Person(int =10,string = "zhansdfa");
friend void show(Person *p);//声明show为友元函数
private:
int age;
string name;
};
void show(Person *pp){
cout << "age"<< pp->age<<pp->name<<endl;
}
int main(){
Person *p = new Person(11,",man");
show(p);
return 0;
}
//2.将其他类中的成员函数声明为友元函数
class MPhone;
class Person{
public:
Person(int =10,string = "zhansdfa");
void show(MPhone *p);//声明show为友元函数
private:
int age;
string name;
};
class MPhone{
public:
MPhone(string);
friend void Person::show(MPhone *pp);
private:
string name;
};
MPhone::MPhone(string s):name(s){}
Person::Person(int a,string s):age(a),name(s){}
void Person::show(MPhone *pp){
cout <<this->name << "phone's"<< pp->name <<endl;
}
int main()
{
Person *p = new Person(11,"man");
MPhone *m = new MPhone("hw");
p->show(m);
return 0;
}
友元类
当一个类为另一个类的友元时,称这个类为友元类。友元类的所有成员函数都是另一个类中的友元成员。
语法形式:friend [class] 友元类名
- 类之间的友元关系不能传递
- 类之间的友元关系是单项的
- 友元关系不能被继承
class HardDisk{
public:
HardDisk(int = 1000);
friend class Computer;
private:
int speed;
string brand;
};
HardDisk::HardDisk(int sped) : speed(sped), brand("hd") {}
class Computer{
public:
Computer(HardDisk hd);
void start();
private:
string name;
string cpu;
HardDisk HD;
};
Computer::Computer(HardDisk hd):name("man"),cpu("i5"){
cout<<"chaungjian computer..."<<endl;
this->HD = hd;
cout << "hd name:" << this->HD.brand << ",speed:" << this->HD.speed << endl;
}
void Computer::start(){
cout << "computer start..." << endl;
}
int main()
{
HardDisk hd(5000);
Computer cp(hd);
cp.start();
return 0;
}
类与结构体的区别
- c++中使用结构体是为了保证与C语言程序的兼容性
- c语言中的结构体不允许定义函数,且没有访问控制权限的属性。
- c++为结构体引入了成员函数,访问控制权限,继承、多态等面向对象特性
- C语言中空结构体大小为0,而c++中空结构体大小为1
- class中成员默认是private,struct中成员默认是public
- class继承默认是private继承,而struct继承默认是public继承
- class可以使用模板,而struct不能
struct person
{
void show();
string name;
int age;
};
void person::show(){
cout << name << age;
}
int main(int argc, char const *argv[])
{
person p;
p.name = "man";
p.age = 16;
p.show();
return 0;
}
继承和派生
概述:继承是在一个已有类的基础上建立一个新类,已有的类成为基类或父类,新建立的类称为派生类和子类;派生和继承是一个概念,角度不同。
- 一个父类可以派生多个子类,一个子类可以继承自多个基类
//子类的声明,继承的方式可以选择,默认为private,还有public,protected
class 子类名:[继承方式]基类名{
子类新增的成员声明;
};
继承方式:
-
public:基类的public成员和protected成员的访问属性保持不变,私有成员不可见。
-
private:基类的public成员和protected成员成为private成员,只能被派生类的成员函数直接访问,私有成员不可见。
-
protected:基类的public成员和protected成员成为protected成员,只能被派生类的成员函数直接访问,私有成员不可见。
-
利用using关键字可以改变基类成员再派生类中的访问权限。using只能修改基类中public和protected成员的访问权限。
class Base{
public:
void show();
protected:
int aa;
double dd;
};
void Base::show(){cout <<aa<<endl; }
class Person:public Base{
public:
using Base::aa; //将基类的protected成员变成public
using Base::show;
private:
//将基类的public成员变成private
using Base::dd;
string name="man";
};
int main()
{
Person *p = new Person();
p->aa = 12;
//p->dd = 12.3; 出错
p->show();
delete p;
return 0;
}
派生类的构造函数和析构函数
- 先执行基类的构造函数,随后执行派生类的构造函数
- 县知县派生类的析构函数,再执行基类的析构函数
- 派生类的构造函数
//派生类名(总参数列表):基类名(基类参数列表),子对象名1(参数列表){函数体;}
Person::Person(int a,double d,string str):Base(a,d),name(str)
{
cout << "Person Class 构造函数!!!" << endl;
}
多继承
一个子类同时继承多个父类的行为
多继承派生类声明方式:
class 派生类名:继承方式1 基类1,继承方式2 基类2{
派生类主体;
};
多重继承派生类的构造函数:
派生类名(总参数列表):基类名1(基类参数列表1),基类名2(基类参数列表2),
子对象名1,...(参数列表)
{
构造函数体;
}
二义性问题:多个基类中有同名成员,出现访问不唯一的问题
- 类名::同名成员名;
- 派生类定义同名成员,访问的就是派生类的同名成员。
虚基类
c++引入虚基类使得派生类再继承间接共同基类时只保留一份同名成员
- 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个共享的基类就成为虚基类(Vritual Base Class)
- 派生类的同名成员比虚基类的优先级更高
//虚基类的声明: class 派生类名:virtual 继承方式 基类名
class A{//虚基类
protected:
int a=111;
};
class B: virtual public A{
protected:
int b=222;
};
class C:virtual public A{
protected:
int c=333;
};
class D:public B,public C{
public:
int d;
void show(){
//如果B和C中都using或定义了a,那么此处会产生二义性问题,应明确声明是哪个基类
cout<<b<<c<<a<<endl;
}
};
int main()
{
D d;
d.show();
return 0;
}
多态和虚函数
向上转型
数据类型的转换,编译器会将小数部分直接丢掉
- 只能将派生类赋值给基类(c++中成为向上转型):派生类对象赋值给基类对象、将派生类指针赋值给基类指针、派生类引用赋值给基类引用
- 派生类对象赋值给基类对象,舍弃派生类新增的成员;派生类指针赋值给基类指针,没有拷贝对象的成员,也没有修改镀锡本身的数据,仅仅是改变了指针的指向;派生类引用赋值给基类引用也和指针一样
上转型后通过基类的对象、指针、引用只能访问从基类继承过去的成员(包括成员变量和函数),不能访问派生类新增的成员
多态
不同的对象可以使用同一个函数名调用不同内容的函数。
- 静态多态性:在程序编译时系统就决定调用哪个函数,比如函数的重载和静态多态性
- 动态多态性:在程序运行过程中动态确定调用哪个函数,通过虚函数实现
虚函数
实现程序多态的一个重要手段,使用基类对象指针访问派生类对象的同名函数
- 将基类中的函数声明为虚函数,派生类中的同名函数自动为虚函数
- 声明形式
virtual 函数类型 函数名(参数列表)
- 构造函数不能是虚函数,析构函数可以声明为虚函数
class A{
public:
virtual void show(){
cout << "A show" <<endl;
}
};
class B: public A{
public:
void show(){
cout <<"B show"<<endl;
}
};
int main()
{
B b;
b.show();
A *pA = &b;
pA->show();//B show 如果show方法前没用virtual声明为虚函数,这里会输出A show
return 0;
}
纯虚函数
在基类中不执行具体操作,只为派生类提供统一结构的虚函数,将其声明为虚函数
class A{
public:
virtual void show()=0;
};
class B: public A{
public:
void show(){
cout <<"B show"<<endl;
}
};
抽象类:包含纯虚函数的类称为抽象类。由于纯虚函数不能被调用,所以不能利用抽象类创建对象,又称抽象基类。
运算符重载
所谓重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。
-
运算符重载是通过函数实现的,它的本质上是函数重载
-
允许重载的运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDb5e5sF-1657612698907)(image-20220708105939431-16572491824701.png)]
-
不允许重载的运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jkWrzFRc-1657612698908)(image-20220708110328836-16572494129562.png)]
定义
重载运算符的规则:
- 不可以自己定义新的运算符
- 不能改变运算符运算对象的个数。
- 不能改变运算符的优先级和结合性
- 应与标准类型运算功能类似
类的运算符重载可以用于两个类之间进行算术运行(加减乘除等等)和其他一切已存在的运算符重载(new [])等都可以通过重载运算符函数operatorOP()进行重载(其中OP为运算符,例如+)。
形式
运算符重载的形式有两种:重载函数作为类的对象,重载函数作为类的友元函数
- 双目运算符作为友元函数时需要定制两个参数
- 运算符重载函数作为类成员函数可以显式调用
//举个🌰
class Vector3{
private:
double m_x;
double m_y;
double m_z;
public:
Vector3():m_x(0.0),m_y(0.0),m_z(0.0){}
Vector3(double x,double y,double z):m_x(x),m_y(y),m_z(z){}
public:
Vector3 operator+(const Vector3 &A)const;
Vector3 operator++();
Vector3 operator--(int);
friend Vector3 operator*(const Vector3 &A,const Vector3 &B);
void show();
};
//运算符重载
Vector3 Vector3::operator+(const Vector3 &A) const{
Vector3 B;
B.m_x = m_x + A.m_x;
B.m_y = m_y + A.m_y;
B.m_z = this->m_z + A.m_z;
return B;
}
Vector3 Vector3::operator++(){
m_x++;
m_y++;
m_z++;
return *this;
}
Vector3 Vector3::operator--(int){
m_x--;
m_y--;
m_z--;
return *this;
}
Vector3 operator*(const Vector3 &A,const Vector3 &B) {
Vector3 tmp;
tmp.m_x = B.m_x * A.m_x;
tmp.m_y = B.m_y * A.m_y;
tmp.m_z = B.m_z * A.m_z;
return tmp;
}
void Vector3::show() {
cout<<m_x<<","<<m_y<<","<<m_z<<endl ;
}
int main()
{
Vector3 A(1,2,3),B(1,1,1),C(2,2,2),D,E;
D = A+B;
D.show();
++B;
B.show();
B.operator++().show();
B--.show();
E = A*C;
E.show();
return 0;
}
1.自增自减:
//前置运算符 ++a --a
operator++()
operator--()
operator++(Vector3 &v)
operator--(Vector3 &v)
//后置运算符 a-- a++
operator++(int)
operator--(int)
operator++(Vector3 &v,int)
operator--(Vector3 &v,int)
2.赋值运算符:
//类内声明(可以类内直接定义)
Vector3& operator=(const Vector3 &A);
//类外定义
Vector3& Vector3::operator=(const Vector3 &A){
if (this != &A){
m_x = A.m_x;
m_y = A.m_y;
m_z = A.m_z;
m_sum = A.m_sum;
}
return *this;
}
3.输入\输出运算符重载
friend ostream &operator<<( ostream &output, const Vector3 &v ){
output << "F : " <<v.m_x<< " I : " << v.m_y<<v.m_z;
return output;
}
friend istream &operator>>( istream &input, Vector3 &v ){
input >> v.m_x>> v.m_y>>v.m_z;
return input;
}
类型转换
- 不指定函数类型和参数,返回值的类型由类型名来确定。
- 类型转换函数只能作为成员函数,不能作为友元函数
//类型转换的形式
//类内定义
public:
operator double(){
return m_sum;
}
int main()
{
Vector3 A(1,2,3);
double d = A;
cout << d<<endl; //6
return 0;
}
数据抽象
数据抽象是指,指向外界提供关键信息,并隐藏其后台的实现细节。数据抽象依赖于接口和实现分离的编程(设计)技术。
举个🌰:一台电脑,您可以使用,但是并不知具体的内部实现细节。这样我们就可以说电脑的内部实现和外部接口分离开了。
访问标签强制抽象
在c++中,我们使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签:
- 使用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
- 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。
访问标签出现的频率没有限制。每个访问标签指定了紧随其后的成员定义的访问级别。指定的访问级别会一直有效,直到遇到下一个访问标签或者类主题的关闭右括号位置。
数据抽象的好处
- 类的内部收到保护,不会因为用户级错误导致对象状态受损
- 类实现可能随着时间的推移而产生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告
如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。
class A{
public:
// 构造函数
A(int i = 0):total(i){}
// 对外的接口
void addNum(int number){
total += number;
}
// 对外的接口
void show(){
cout << total << endl;
}
private:
// 对外隐藏的数据
int total;
};
int main( )
{
A a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
a.show();//60
return 0;
}
抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。
数据封装
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。这样能避免受到外界的干扰和误用,从而确保了安全。
C++ 通过创建类来支持封装和数据隐藏。类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。私有成员意味着它们只能被 Box 类中的其他成员访问,而不能被程序中其他部分访问。这是实现封装的一种方式。
接口(抽象类)
接口:描述类的行为和功能,而不需要完成类的特定实现
C++的接口是使用抽象类来实现的,抽象类和数据抽象互不混淆,数据抽象是把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明过为纯虚函数,则这个类就是抽象类。
抽象类的设计目的是为了给其他类提供一个可以继承的适当的基类。抽象类不能实例化,只能作为接口使用。因此,抽象类的子类需要被实例化,则必须实现每个虚函数。可用于实例化对象的类成为具体类。
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。
文件和流
流—一连串连续不断的数据集合
流类和对象
- 输入流—从输入设备流向内存的流
- 输出流—从内存中流到设备的流
- 内存缓冲区—用来存放流中的数据
iostream 标准库,它提供了 cin 和 cout 方法分别用于从标准输入读取流和向标准输出写入流。
C++ 中另一个标准库 fstream,它定义了三个新的数据类型:
数据类型 | 描述 |
---|---|
ofstream | 该数据类型表示输出文件流,用于创建文件并向文件写入信息。 |
ifstream | 该数据类型表示输入文件流,用于从文件读取信息。 |
fstream | 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。 |
进行文件处理前,必须要在C++源代码文件中包含头文件<iostream>和<fstream>
打开文件
//第一个参数指定要打开文件的名称和位置,第二个参数定义文件被打开的模式
void open(const char *filename,ios::openmode mode);
//打开方式可以结合使用
ifstream afile;
afile.open("file.dat", ios::out | ios::in );
//关闭文件
afile.close();
模式标志 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
#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);//只能接受char类型
// 向文件写入用户输入的数据
outfile << data << endl;
cout << "Enter your age: ";
cin >> data;
cin.ignore();//可以接收string
// 再次向文件写入用户输入的数据
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;
}
异常处理
C++ 异常处理涉及到三个关键字:try、catch、throw。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
抛出异常
可以使用throw在代码的任何地方抛出异常。throw语句的操作数可以是任何表达式,表达式的结果的类型决定了抛出的异常类型
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
捕获异常
catch在try后边,用于捕获异常,可以指定捕捉的异常类型,这是由catch关键字后的括号内的异常声明决定
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 异常的代码
}
定义新的异常
可以通过重载和继承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)
{
//其他的错误
}
}
动态内存
//一维数组 动态分配,数组长度为 m
int *array=new int [m];
//释放内存
delete [] array;
//二维数组 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
int **array
array = new int *[m];
for( int i=0; i<m; i++ ){
array[i] = new int [n] ;
}
//释放
for( int i=0; i<m; i++ ){
delete [] array[i];
}
delete [] array;
//对象的动态内存分配与简单的数据类型类似。
//如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
class Box{};
int main(){
Box* myBArray = new Box[4];
delete [] myBArray;
return 0;
}
命名空间
可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
举一个计算机系统中的例子,一个文件夹(目录)中可以包含多个文件夹,每个文件夹中不能有相同的文件名,但不同文件夹中的文件可以重名。
// 定义命名空间
namespace namespace_name{ }
//调用命名空间
name::code; //code可以是变量或者函数
//🌰🌰🌰
namespace my_space {
void func(){
cout<<"my_space"<<endl;}
}
namespace me_space {
void func(){
cout<<"me_space"<<endl;}
}
int main()
{
my_space::func();
me_space::func();
return 0;
}
使用using指令,使用using namespace
指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。
namespace my_space {
void func(){
cout<<"my_space"<<endl;}
}
namespace me_space {
void func(){
cout<<"me_space"<<endl;}
}
using namespace my_space;
using my_space::func; //可以使用这种方式来指定命名空间中特定项目
int main()
{
//这两句效果相同
func();
my_space::func();
return 0;
}
不连续的命名空间
命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。
嵌套命名空间
//命名空间可以嵌套,您可以在一个命名空间中定义另一个命名空间
namespace namespace_name1 {
// 代码声明
namespace namespace_name2 {
// 代码声明
}
}
// 访问 namespace_name2 中的成员
using namespace namespace_name1::namespace_name2;
// 访问 namespace_name1 中的成员
using namespace namespace_name1;
C++模板
- 模板是泛型编程的基础,泛型编程是一种独立于任何特定类型的方式编写代码
- 模板是创建泛型类或函数的蓝图或公司。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
- 每个容器都有一个单一的定义,比如向量,我们可以定义许多不同类型的向量,比如vector <int>或vector <string>
函数模板
template <typename type>
//🌰🌰🌰
//不允许template与函数模板定义之间有任何语句(注释也不行)
template<typename T>
inline T const& Max(T const& a,T const& b){
return a > b ? a:b;
}
int main()
{
int i=39,j=20;
cout<<"Max(i,j):"<< Max(i,j) <<endl;
double f1 = 12.3, f2=23.4;
cout <<"Max(f1,f2):"<<Max(f1,f2)<<endl;
string s1="Hello",s2="World";
cout <<"Max(s1,s2):"<<Max(s1,s2)<<endl;
return 0;
}
类模板
用来设计结构和成员函数完全相同,但是所处理的数据类型不同的通用类
template <class type> class class-nameP{ };
template<class Type>
class Compare{
private:
Type x,y;
public:
Compare(Type a,Type b){
this->x=a;
this->y=b;
}
Type max(){
return x > y ? x:y;
}
Type min(){
return x < y ? x:y;
}
};
int main(){
Compare<int> c(1,6);
c.max();
c.min();
return 0;
}
多线程
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个及以上的程序。一般情况下,两种类型的多任务处理:基于进程和基于线程
- 基于进程的多任务处理是程序的并发执行
- 基于线程的多任务处理是同一程序的片段的并发执行
#include <iostream>
#include <pthread.h>
using namespace std;
#define NUM_THREADS 5
void* say_hello(void* args){
cout<<"hello runoob!" << endl;
return 0;
}
int main()
{
// 定义线程id变量,多个变量使用数组
pthread_t tids[NUM_THREADS];
for (int i=0;i<NUM_THREADS;i++) {
//参数依次是,线程id,线程参数,调用的函数,传入的函数参数
int ret = pthread_create(&tids[i],NULL,say_hello,NULL);
if(ret != 0){
cout << "pthread_create error:error_code=" <<ret<< endl;
}
}
//等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来
pthread_exit(NULL);
}
# 在Linux使用命令: g++ test.cpp -lpthread -o test.o
//接收参数并输出参数
#include <iostream>
#include <cstdlib>
#include <pthread.h>
using namespace std;
#define NUM_THREADS 5
void *PrintHello(void *threadid)
{
// 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取
int tid = *((int*)threadid);
cout << "Hello Runoob! 线程 ID, " << tid << endl;
pthread_exit(NULL);
}
int main ()
{
pthread_t threads[NUM_THREADS];
int indexes[NUM_THREADS];// 用数组来保存i的值
int rc,i;
for( i=0; i < NUM_THREADS; i++ ){
cout << "main() : 创建线程, " << i << endl;
indexes[i] = i; //先保存i的值
// 传入的时候必须强制转换为void* 类型,即无类型指针
rc = pthread_create(&threads[i], NULL,
PrintHello, (void *)&(indexes[i]));
if (rc){
cout << "Error:无法创建线程," << rc << endl;
exit(-1);
}
}
pthread_exit(NULL);
}
//向线程传递参数
#include <iostream>
#include <pthread.h>
#include <cstdlib>
using namespace std;
#define NUM_THREADS 5
struct thread_data
{
int thread_id;
char *message;
};
void* say_hello(void *args){
struct thread_data *my_data;
my_data = (struct thread_data *) args;
cout <<"thread id:"<<my_data->thread_id;
cout <<"Message:"<<my_data->message<<endl;
pthread_exit(NULL);
}
int main()
{
// 定义线程id变量,多个变量使用数组
pthread_t tids[NUM_THREADS];
struct thread_data td[NUM_THREADS];
int rc,i;
for (int i=0;i<NUM_THREADS;i++) {
cout << "main():creating thread,"<<i<<endl;
td[i].thread_id = i;
td[i].message = (char *)"this is message";
rc = pthread_create(&tids[i],NULL,say_hello,(void *)&td[i]);
if(rc){
cout<<"Error:unable to create thread"<<rc<<endl;
exit(-1);
}
}
//等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来
pthread_exit(NULL);
}
# 在Linux使用命令: g++ -Wno-write-strings main.cpp -lpthread -o test.o
连接和分离线程
我们可以使用以下两个函数来连接或分离线程:
pthread_join (threadid, status)
pthread_detach (threadid)
pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连接。
// 连接和分离线程
#include <iostream>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#define NUM_THREADS 5
void *wait(void *t)
{
int i;
long tid;
tid = (long)t;
sleep(1);
cout << "Sleeping in thread " << endl;
cout << "Thread with id : " << tid << " ...exiting " << endl;
pthread_exit(NULL);
}
int main ()
{
int rc;
int i;
pthread_t threads[NUM_THREADS];
pthread_attr_t attr;
void *status;
// 初始化并设置线程为可连接的(joinable)
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for( i=0; i < NUM_THREADS; i++ ){
cout << "main() : creating thread, " << i << endl;
rc = pthread_create(&threads[i], NULL, wait, (void *)&i );
if (rc){
cout << "Error:unable to create thread," << rc << endl;
exit(-1);
}
}
// 删除属性,并等待其他线程
pthread_attr_destroy(&attr);
for( i=0; i < NUM_THREADS; i++ ){
rc = pthread_join(threads[i], &status);
if (rc){
cout << "Error:unable to join," << rc << endl;
exit(-1);
}
cout << "Main: completed thread id :" << i ;
cout << " exiting with status :" << status << endl;
}
cout << "Main: program exiting." << endl;
pthread_exit(NULL);
}
thread
c++11之后添加的新的标准线程库std::thread
在<thread>头文件中声明
//使用c++11
g++ -std=c++11 test.cpp
std::thread 默认构造函数,创建一个空的 std::thread 执行对象。
#include <thread>
std::thread thread_object(callable)
一个可调用对象可以是以下三个中的任何一个:
- 函数指针
- 函数对象
- lambda 表达式
// 演示多线程的CPP程序
// 使用三个不同的可调用对象
#include <iostream>
#include <thread>
using namespace std;
// 一个虚拟函数
void foo(int Z)
{
for (int i = 0; i < Z; i++) {
cout << "线程使用函数指针作为可调用参数\n";
}
}
// 可调用对象
class thread_obj {
public:
void operator()(int x)
{
for (int i = 0; i < x; i++)
cout << "线程使用函数对象作为可调用参数\n";
}
};
int main()
{
cout << "线程 1 、2 、3 "
"独立运行" << endl;
// 函数指针
thread th1(foo, 3);
// 函数对象
thread th2(thread_obj(), 3);
// 定义 Lambda 表达式
auto f = [](int x) {
for (int i = 0; i < x; i++)
cout << "线程使用 lambda 表达式作为可调用参数\n";
};
// 线程通过使用 lambda 表达式作为可调用的参数
thread th3(f, 3);
// 等待线程完成
// 等待线程 t1 完成
th1.join();
// 等待线程 t2 完成
th2.join();
// 等待线程 t3 完成
th3.join();
return 0;
}
// 使用c++11来运行程序g++ -std=c++11 test_thread.cpp -lpthread -o test_thread.o
C++STL
C++STL是一套c++模板类,提供了通用的模板类和函数,可以实行多种流程和常用的算法和数据结构,如:向量、链表、队列、栈。
(有说六大组件:容器、算法、迭代器、适配器(为算法提供更多接口)、仿函数(为算法提供策略)、空间配置)
C++ 标准模板库的核心包括以下三个组件:
组件 | 描述 |
---|---|
容器(Containers) | 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。 |
算法(Algorithms) | 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。 |
迭代器(iterators) | 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。算法借助迭代器去操作容器 |
这三个组件都带有丰富的预定义函数,帮助我们通过简单的方式处理复杂的任务。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// 创建一个向量存储 int
vector<int> vec;
int i;
// 显示 vec 的原始大小
cout << "vector size = " << vec.size() << endl;
// 推入 5 个值到向量中
for(i = 0; i < 5; i++){
vec.push_back(i);}
// 显示 vec 扩展后的大小 ,显示vec扩展之后的容量vec.capacity()
cout << "extended vector size = " << vec.size() << endl;
// 访问向量中的 5 个值
for(i = 0; i < 5; i++){
cout << "value of vec [" << i << "] = " << vec[i] << endl;
}
// 使用迭代器 iterator 访问值
vector<int>::iterator v = vec.begin();
while( v != vec.end()) {
cout << "value of v = " << *v << endl;
v++;
}
for(v = vec.begin();v!=vec.end();v++){
cout << "value of v = " << *v << endl;
}
return 0;
}
- push_back( ) 成员函数在向量的末尾插入值,如果有必要会扩展向量的大小。
- size( ) 函数显示向量的大小。
- begin( ) 函数返回一个指向向量开头的迭代器。
- end( ) 函数返回一个指向向量末尾的迭代器。
迭代器
迭代器是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访问容器元素。按照定义方式分为以下四种。
1.正向迭代器:容器类名::iterator 迭代器名;
2.常量正向迭代器:容器类名::const_iterator 迭代器名;
3.反向迭代器:容器类名::reverse_iterator 迭代器名;
4.常量反向迭代器:容器类名::const_reverse_iterator 迭代器名
容器
vector容器:单端动态数组容器,vector是一个类模板,使用时要指定元素类型。
顺序容器:可变长动态数组Vector、双端队列deque、双向链表list
关联容器:set、multliset、map、multimap
- 关联容器内的元素是排序的,所以查找时具有非常好的性能。
容器适配起:栈stack、队列queu、优先队列priority_queue
所有容器具有的函数:
int size();
bool empty();
- 顺序容器和关联容器函数:
begin() //容器的起始迭代器
end() //容器的结束迭代器(尾元素的下一个元素位置)
rbegin() //尾元素
erase(...)
clear()
- 顺序容器独有的函数:
front()
back()
push_back(); //向尾部插入一个数据
pop_back(); //删除尾元素
insert(...);
//定义一个vector容器
vector<int> v1;
//定义一个迭代器iterator 保存的是元素的位置
vector<int>::iterator it = v1.begin();
for(; it!=v1.end();it++){
cout << *it << endl;
}
- 🎶 string - 字符串
- 🚃 vector - 向量
- ➿ deque - 双向队列
- 🍡 stack - 栈
- 🏁 queue - 队列
- 📜 list - 链表
- 🏵 set / multiset - 集合
- 🗺 map / multimap - 映射
- 🗒 容器简单小结
仿函数(Functor)
函数对象/仿函数
- 重载函数调用操作符的类,其对象常称为函数对象(function object),也叫仿函数(functor),使得类对象可以像函数那样调用。
- STL提供的算法往往有两个版本,一种是按照我们常规默认的运算来执行,另一种允许用户自己定义一些运算或操作,通常通过回调函数或模版参数的方式来实现,此时functor便派上了用场,特别是作为模版参数的时候,只能传类型。
- 函数对象超出了普通函数的概念,其内部可以拥有自己的状态(其实也就相当于函数内的static变量),可以通过成员变量的方式被记录下来。
- 函数对象可以作为函数的参数传递。
- 函数对象通常不定义构造和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用时的运行时问题。
- 模版函数对象使函数对象具有通用性,这也是它的优势之一。
- STL需要我们提供的functor通常只有一元和二元两种。
- lambda 表达式的内部实现其实也是仿函数
算法
STL 提供能在各种容器中通用的算法(大约有70种),如插入、删除、查找、排序等。算法就是函数模板。算法通过迭代器来操纵容器中的元素。
STL 中的大部分常用算法都在头文件 algorithm 中定义。此外,头文件 numeric 中也有一些算法。
许多算法操作的是容器上的一个区间(也可以是整个容器),因此需要两个参数,一个是区间起点元素的迭代器,另一个是区间终点元素的后面一个元素的迭代器。
会改变其所作用的容器。例如:
- copy:将一个容器的内容复制到另一个容器。
- remove:在容器中删除一个元素。
- random_shuffle:随机打乱容器中的元素。
- fill:用某个值填充容器
不会改变其所作用的容器。例如:
- find:在容器中查找元素。
- count_if:统计容器中符合某种条件的元素的个数。
ool empty();