基础部分
输入和输出
“输入输出流”是指由若干个字节组成的字节序列,按照一定的顺序从一个对象传送到另一个对象,即从“源”到“目的端”的流动,例如在输出时字节从内存流向输出设备,输入时字节从输入设备流向内存。
一、C++(CPP)中引用的头文件,输入和输出于C语言不同,在C语言中,输入输出头文件为#include<stdio.h>,而C++中输入输出头文件为#include<iostream>;
二、C语言中头文件命名方式为#include<XXX.h>,而C++中头文件命名不带后面的.h
三、输入"cin"为控制台读入内容到变量:
#include<iostream>
using namespace std; //std标准命名空间,所有函数或者对象都是在std中定义
int main{
int a = 1;
cin >> a;
return 0;
}
四、输出"cout"从变量输出内容到控制台:
#include<iostream>
using namespace std;
int main{
int a = 2;
cout << a << endl; //endl为换行
return 0;
}
五、std标准命名空间:C++标准库中的函数或者对象都是在命名空间std中定义。
bool类型
bool类型是C++中的基本数据类型,为一种整型类型。
* bool类型单独占据一个字节即八个二进制位,取值true(1)或false(0)。
* 任何非0数都为true,0转换为false。
* C语言中不存在bool类型。
#include<iostream>
using namespace std;
int main{
bool a = -1; //bool类型非0的-1转换为1
int b = 5; //int整型 5
int c = 0; //int整型 0
bool d = 6; //bool类型非0的6转换为1
cout << a << endl;//1
cout << b << endl;//5
cout << c << endl;//0
cout << d << endl;//1
return 0;
}
引用
参数值与引用
先前我们学习过两种参数传递的方式:第一种为值传递,第二种为地址传递。接下来我们来学习一个新知识:引用,什么是引用呢?总的来说就是给变量起别名。
引用作为函数参数时的原因:
* 在函数内部会对此参数进行修改
* 提高函数调用和运行效率
在函数调用时,值传递为将实参拷贝到形参。而引用则不需拷贝,引用已具有实参值的信息,节省了值拷贝的时间,和空间的消耗,当程序对时间成本有要求时,非常必要。
下面用交换函数swap来演示三种函数的参数传递:
#incldue<iostream>
using namespace std;
void swap1(int a,int b){ //值传递,函数中形参不能对实参进行修改
int temp = a;
a = b;
b = temp;
}
void swap2(int* a,int* b){ //地址传递,通过传入地址,基于地址对实参的值进行修改
int temp = *a;
*a = *b;
*b = temp;
}
void swap3(int &a,int &b){ //引用传递,通过起别名,对实参的值进行修改
int temp = a;
a = b;
b = temp;
}
int main{
int a = 10;
int b = 10;
swap1(a,b);//不能交换
swap2(&a,&b);//能交换
swap3(a,b);//能交换
return 0;
}
返回值与引用
* 返回值为引用的好处:在内存中不产生返回值的副本。
当返回值不为引用时,由于函数中的形参为局部变量,当函数结束之后会被释放,所以在传值的过程中形参会产生一个副本,传递到实参中。
而当返回值为引用,虽然函数中形参为局部变量函数结束之后被释放,但是由于返回值为引用即为“起别名”,所以不需产生副本就可以直接将值传递到实参中。
int fun1(int a){ //形参,局部变量,a存放到栈区,函数结束之后a被释放,产生副本传递到实参
a += 2;
return a;
}
int &fun2(int a){ //形参,局部变量,函数结束a释放,引用起别名fun2,不产生副本直接将数值传递
a += 2;
return a;
}
引用和指针的区别:
* 引用在定义时必须被初始化(底层为指针)并且不能被初始化成NULL,指针没有要求。
int a = 10; //b 初始化了,无错误
int &b = a;
int a = 10; //b 未初始化,有错误
int &b;
b = a;
* 引用不能改变引用关系,指针随意。指针:改变指向方向即可,引用:不论怎么修改都是改变最初关系的值。
int a = 10;
int b = 11;
int &c = a;
c = b;
b = 12;
c = 13;
cout<< a << " " << b << " " << c <<endl;//不论怎么改变b的值,c的值永远和a相同,输出13 12 13
* 有多级指针没有多级引用
* 引用++和指针++表达的含义不同。指针自增是在指针上的计算(sizeof(int) * 4或者8)指向下一个空间,引用自增单纯加一。
* 指针用sizeof计算大小结果不同,32位内存下是4字节,64位内存下是8字节,引用结果取决于类型的大小。
* 指针是一个实体,需分配内存空间;引用知识变量的别名,不需要分配内存空间。
函数参数默认值
在函数声明或者函数定义的时候直接给形参赋值,这样在函数调用的时候就不用再给形参传值,则会使用它的默认值:
ps:参数默认值必须从右向左依次赋值
void fun0(int a,int b = 10,int c = 6){
cout << a << b << c <<endl;
}
void fun1(int a = 1,int b,int c = 6){//报错,默认实参不在形参列表的结尾
cout << a << b << c <<endl;
}
int main()
{
fun0(1,1);//a = 1,b的值被覆盖b = 1,c = 6默认值
}
vector使用
C++中创建数组需要引用头文件#include<vector>
vec.push_back(1); //在vec数组最后添加一个元素“1”,void类型 |
vec.pop_back(); //移除vec数组最后一个元素,void类型 |
vec.back(); //返回vec数组最后一个元素,int类型 |
vec.front(); //返回vec数组第一个元素,int类型 |
vec.size(); //返回vec数组中元素数量,int类型 |
vec.capacity(); //返回vec数组中所能容纳的元素数量,在不重新分配内存的情况下,int类型 |
vec.resize(n);或vec.resize(n,m); //改变vec数组元素数量大小,若size大于删除大于n的部分若size小于n则 //默认补 “0”,若按照第二种则都补充“m”,void类型 |
vec.clear(); //清楚vec数组所有元素,size() = 0,capacity()不变,void类型 |
vec.empty(); //判断vec数组是否为空数组,是则返回“1”,不是则返回“0”,int类型 |
vec1.swap(vec2); //交换vec1和vec2两个数组,即名字交换 |
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> vec = { 1,2,3,4,5 };
vec.push_back(1);//在数组最后一位的后面添加一个元素
vec.pop_back();//移除数组最后一个元素
cout << "返回数组中最后一个元素" << endl << vec.back() << endl;//back()返回数组中最后一个元素
cout << "返回数组中第一个元素" << endl << vec.front() << endl;//front()返回数组中第一个元素
cout << "返回数组中元素的数量" << endl << vec.size() << endl; //size()返回数组中元素数量
cout << "返回数组所能容纳的元素的数量" << endl << vec.capacity() << endl;//capacity()返回数组中所能容纳元素的数量
vec.resize(6,6);//resize()改变数组中含有元素数量的大小,多删少补
cout << "返回元素是否为空" << endl << vec.empty() << endl;//empty()判断数组是否非空(是否还存有元素),空为1,不空为1
vec.clear();
for (auto it : vec) {
cout << it << " ";
}
vector<int> vec1 = {1,2};
vec.swap(vec1);//swap()交换两个数组,vec.swap(vec1),vec变成vec1,vec1变成vec类似将两个数组名字交换
for (auto it : vec) {
cout << it << " ";
}
vec.clear();
cout << "返回元素是否为空" << endl << vec.empty() << endl;//empty()判断数组是否非空(是否还存有元素),空为1,不空为1
return 0;
}
一维数组
vector<int> vec;
//vec[0] = 1;//下标越界
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);//vec = {1,2,3}
vec[1] = 22;//vec中只能修改元素不能插入元素
for(auto it :vec){
cout << it << " ";//增强for循环 it代表vec
}
****
vector<int> vec = {1,2,3};//创建一个新数组 有{1,2,3}
vector<int> vec(3,4);//创建数组有3个4
vector<int> vec(5);//创建数组 5个0
vector<int> vec;//创建空数组
二维数组
vector<vector<int>> vec = {
{1,2,3},
{4,5,6},
{7,8,9}
};
//在第1行末尾添加一个元素999
vec[1].push_back(999);//{1,2,3,999}
vec.push_back({6,6,6});//添加一行新元素{6,6,6}
vector<vector<int>> vec1(3,vector<int>(5,5));//创建一个二维数组 三行 5列 都是5
vec.back()[2] = 7//把最后一行第三个元素换成7
访问修饰符(把属性设置为私有)
类中每个成员都有自己的访问修饰符,可以是三种情况之一:
- private 私有的,只有当前类内才有权限访问这个成员
- protected 受保护的,类内和子类有权限访问这个成员
- public 公共的,所有位置都有权限访问这个成员
class people{ private: double weight; protected: double wealth; public: string name; int age; void smoking(); }; void people :: smoking(){ cout << "smoking" << endl; weight = 1;//私有的在类内可以访问 wealth = 100;//受保护的可以在类内和子类访问 } int main(){ people p; p.name; }
面向对象的三大特性
- 封装:把属性(成员变量)和操作(成员函数)结合为一个独立的整体;
- 继承:子类继承父类的特征和行为,使得子类对象(实例)具有父亲的实例域和方法,或子类遵从父类继承方法,使得子类具有父类相同的行为。
- 声明是告诉编译器变量或函数的名字,不会为变量分配空间
- 定义时对这个变量或函数进行内存分配和初始化,需要分配空间,同一个变量可以被声明多次但只能定义一次
构造函数
c++利用构造函数实现了对象所占内存的赋值。对象初始化时强制执行构造函数,如果我们没有自己实现构造函数,编译器将会提供默认的构造函数。
构造函数语法
- 没有返回值 格式为:“类名 (){}”
- 函数名称与类名相同
- 构造函数可以有参数,可以重载
- 编译器在创建对象的时候会自动调用构造函数
class people{
int a;
public:
int e;
people(){
cout <<"无参构造"<<endl;
}
people (int A){
a = A;
cout <<"A的构造函数"<<endl;
}
people (int A,int B){
cout << "A B的构造函数" <<endl;
}
};
int main(){
people p();//编译器会认为函数声明
people p;//默认调用无参构造
people p(3);
people p(1,2);
}
析构函数
析构函数是作用于对象销毁工作,清空对象内部指针指向的堆区内存。析构函数是在释放对象的时候自动调用,栈区对象自动释放。
析构函数语法
- 没有返回值 格式为:“~类名 (){}”
- 函数名称与类名相同但是函数名称前需要添加一个~
- 构造函数不可以有参数,所以不能重载
- 编译器在对象销毁前会自动调用析构函数,不需要手动调用
class A{
public:
int *p;
A(int n){
p = (int*)malloc(N);
cout << "申请了"<<N<<"个字节"<<endl;
}
~A(){
if(p != NULL) free(p);//释放成员变量指向的堆区内存
cout << "析构函数" <<endl;
}
};
int main(){
A a(40);//栈区对象 编译器会自动释放
}
this
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this的作用域是在类的内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。
this是指相当前对象的指针,哪个对象调用包含this指针的函数,this指向哪个对象
this一般在构造函数中使用,用来区分成员变量和参数
class A{
int num;
public:
//this区分成员变量和参数
A(int num){
this->num = num;
}
}
int main(){
A p(1);
}
new&&delete
#include<iostream>
#include<vector>
using namespace std;
class A{
public:
int *p = nullptr;
A(){}
A(int n){
p = new int[n];
}
~A(){
if(p) delete[]p;
}
}
int main(){
int *i = new int;//在堆区申请一个int类型的内存,值为随机值
delete i;
int *i = new int(3);//在堆区申请一个int类型的内存,值为3
delete i;
int *i = new int[3];//在堆区申请一个int类型的数组,三个元素为随机值。
delete[] i;
int *i = new int[3]{1,2,3};//在堆区申请一个int类型的数组,三个元素为1 2 3。
delete[] i;
A *a = new A();//在堆区创建A类型对象调用无参构造
delete a;
A *a1 = new A(3);
delete a1;//delete 会先调用析构函数在调用free()
A *a1 = new A[3];//在堆区创建一个A类型的数组,调用无参构造
delete[] a1;
A *a1 = new A[3]{A(),A(2),A(3)};//在堆区创建一个A类型的数组,调用无参构造
delete[] a1;
}
new和malloc的区别
- new/delete是c++的运算符,编译时需要添加参数,malloc/free是c语言中的函数,编译时需要头文件的支持
- new返回指定类型的指针,可自动计算出大小。malloc需我们计算字节数,返回后强型转换为实际类型的指针
- malloc只管分配内存,不能初始化,new可对得到的内存进行初始化
- new可调用构造函数,malloc不能
- delete可调用析构函数,free不能
- new/delete都可被重载,malloc/free不能
- new分配内存失败时会抛出异常,而malloc分配失败会返回null
try {
int* ptr = new int[1000000000000]; // 尝试分配非常大的数组
} catch (const std::bad_alloc& e) {
// 内存分配失败,处理异常
std::cout << "内存分配失败: " << e.what() << std::endl;
}
int* ptr = (int*)malloc(1000000000000 * sizeof(int)); // 尝试分配非常大的数组
if (ptr == NULL) {
// 内存分配失败,处理情况
std::cout << "内存分配失败" << std::endl;
} else {
// 内存分配成功,继续使用指针
// ...
}
new malloc free delete可以混合使用吗
- 对于基本类型而言,根据需要new和malloc可以混用,new[]和malloc可以混用,delete、delete[]和free可以混用
- 对于构造函数没有作用的类,new和malloc可以混用。
- 对于没有显示定义析构函数的类,delete、delete[]和free可以混用
- 对于显示定义析构函数的类,delete[]和new[]必须配套使用,delete和free如果想混用free需要显示调用解析函数
优先级队列
#inlcude<iostream>
#inlcude<queue>
using namespace std;
/*
push()//加入一个元素
top()返回堆顶元素
pop()删除堆顶元素 没有返回值
empty()如果为空 true 反之 false
size()返回优先级队列中拥有的元素个数
*/
int main(){
priority_queue<int> q1; //默认优先级队列,最大堆结构
q1.push(-5);
q1.push(554);
q1.push(7);
q1.push(-4);
q1.push(5);
while(!q1,empty()){
cout << q1.top() <<endl;
q1.pop();
}
}
/*
int:表示优先级队列中存放 int类型的元素
vector<int>表示优先级队列由数组模拟
greater<int>表示优先级队列为最小堆
less<int>表示优先级队列为最大堆
*/
priority_queue<int,vector<int>,greater<int>> q1;//greater 最小堆
priority_queue<int,vector<int>,less<int>> q2;//less 最大堆
比较器
#include<iostream>
#inlcude<queue>
#inlcude<vector>
using namespace std;
class A{
public:
int num;
A(int num){
this->num = num;
}
};
//函数比较器
bool cmp(A &a,A &b){
return a.num<b.num;
}
struct CMP{
bool operator()(A &a,A &b){
return a.num<b.num;
}
}
int main(){
vector<A> vec = {A(2),A(-2),A(5),A(3)};
sort(vec.begin(),vec.end(),CMP());//从小到大
for(auto it : vec) cout << it.num <<" ";
}
多继承
一个类可以继承自多个父类,它可以从多个基类继承数据和函数。定义一个派生类,我们是用一个类派生列表来指定有哪些基类,代码如下:
#include<iostream>
#include<string>
using namespace std;
class father{
public:
int pub;
};
class mather{
public:
int pub;
};
clas son :public father,public mather{};
int main(){
son s;
s.father::pub = 1;
s.mather::pub = 1;
return 0;
}
函数重写(覆盖)
定义:子类重新定义父类中有相同名称,返回值和参数的虚函数,主要在继承关系中出现。
基本条件:
- 重写的函数和被重写的函数必须都为virtual函数,并分别位于基类和派生类中
- 重写的函数和被重写的函数,返回值,函数名,函数参数必须完全一致
#include<iostream>
using namespace std;
class father{
public:
virtual void fun(){
cout <<"father"<<endl;
}
};
class child:public father{
public:
virtual void fun(){
cout<<"child"<<endl;
}
};
/*最后输出child,子类中的virtual可以省略*/
函数隐藏
定义:在子类中只要和父类函数名字相同,不是重写就是函数隐藏
隐藏与重写区别:在子类和父类中,函数名相同,参数相同,父类中的同名函数没有virtual为关键字,为隐藏。
#include<iostream>
using namespace std;
class Base{
public:
void fun(){cout << "funA()" <<endl;}
virtual void funB(){cout <<"funB()"<<endl;}
};
class Heri:public Base{
public:
void funA(){cout<<"funA():Heri"<<endl}//函数隐藏,非虚函数,父类中没有virtual
void funA(int a){cout<<"fun A(int a):Heri"<<endl;}//函数隐藏参数不同
void funB(){cout<<"fun B():Heri"<<endl;}//重写 子类省去virtual
}
多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++多态意味着调用成员函数时,会根据调用函数对象的类型来执行不同的函数。
- 静态多态:函数重载(运算符重载),模版属于静态多态,在编译期间就能确定的多态。
- 动态多态:父类的指针或引用指向子类对象,通过指针或引用调用子类重写的虚函数,在运行期间才能确定具体调用哪个函数,是动态多态。
启动动态多态的条件:有继承关系,子类重写父类虚函数并且父类指针调用子类重写的虚函数。
#include<iostream>
using namespace std;
/*
重写:子类和父类中,同名且同形参的虚函数能够发生重写,子类重写父类的虚函数(virtual)
*/
class A{
public:
int a;
int b;
int c;
const int d = 1;
virtual void work(){
cout << "A work()" <<endl;
}
void fun(){
cout << "A fun()" << endl;
}
};
class B:public A{
public:
virtual void work(){
cout << "B重写A" <<endl;
}
void fun(){
cout << "B fun()" <<endl;
}
};
class C:public B{
public:
virtual void work(){
cout << "C重写B" <<endl;
}
void fun(){
cout << "C fun()" <<endl;
}
};
int main(){
//多态 父类指针指向子类对象
//父类指针指向子类对象,去调用虚函数的时候会调用被重写的虚函数
A *a = new C();
a->fun();
a->work();
}
最终输出:A fun()
C重写B
多态的实现
为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术核心就是虚函数表。下面介绍虚函数表是如何实现动态绑定的。
类的虚函数表
- 每个包含了虚函数的类都包含一个虚表(存放虚函数指针的数组)
- 当一个类(B)继承一个类(A)时,类B会继承类A的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这类也拥有自己的虚表
- 下列代码中。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表
class A{
public:
virtual void vfun1();
virtual void vfun2();
void func1();
void func2();
private:
int m_data1,m_data2;
};
class B : public A{};//此时类B也有自己的虚表
class A{
public:
virtual void vfun1();
virtual void vfun2();
void func1();
void func2();
private:
int m_data1,m_data2;
};
class B : public A{
public:
virtual void vfun1();
void func1();
private:
int m_data3;
};//此时类B也有自己的虚表
class C : public B{
public:
virtual void vfun2();
void func2();
private:
int m_data1,m_data4;
};//此时类C也有自己的虚表
初始化参数列表
只能在构造函数里使用该语法,可以给所有成员设置初始化参数,const类型和引用类型必须在初始化参数列表中初始化。成员的构造顺序和在初始化参数列表中的顺序无关,与类中声明顺序有关。
class A
{
public:
int a;
int b;
const int c;
int &d;
A():b(33),a(25),c(3),d(a){}
}
虚析构
虚析构函数就是对虚构函数的析构,可以重写,编译器为了让析构函数实现多态,会将他们的名字处理为destructor,这样就可以构成多态
#include<iostream>
using namespace std;
/*编译器为了让析构函数实现多态 默认在析构函数前加了个destructor,可隐藏 可重写*/
class father {
public:
father() {
cout << "father的构造函数" << endl;
}
virtual ~father() {
cout << "father的析构函数" << endl;
}
};
class child :public father {
public:
child() {
cout << "child的构造函数" << endl;
}
~child() {
cout << "child的析构函数" << endl;
/*编译器在这会默认添加一个父函数的destructor 也就是父函数的析构函数*/
}
};
int main() {
father* f = new child();
delete f;/*如果父函数的析构函数前不加virtual,则编译器将其定义为隐藏,则只会执行父函数的析构,反之执行子函数的但是子函数析构里编译器添加了父函数的析构*/
return 0;
}
纯虚函数与抽象类
- 定义:纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上‘=0’
- 如果父类中一个虚函数其自身的实现无意义。在很多情况下,基类本身生成对象是不合理的。例如,水果可以分成苹果,西瓜,梨,但是水果本身生成对象就不合理。此时可以将该虚函数定义成纯虚函数
- 拥有至少一个纯虚函数的类是纯虚类(抽象类),纯虚类不能直接创建对象。子类只有重写了所有纯虚函数后,才能创建子类对象。
- 一个普通的虚函数在虚函数表中其函数指针就是一个有意义的值,如果是一个纯虚函数,那么在虚函数表中,其函数指针的值就是0
#include<iostream>
using namespace std;
/*具有纯虚函数的类为一个抽象体 只能作为基类 纯虚函数不能直接创建对象*/
/*如果纯虚函数的子类没有重写父类纯虚函数 那么子类也是一个抽象体 不能创建对象*/
/*正常一个虚函数在虚函数表中的函数指针都是一个有意义的值 而纯虚函数是0*/
class A {
public:
virtual void fun() = 0;
};
class B : public A {
public:
void fun() {
cout << "子类B重写了父类A" << endl;
}
};
int main() {
A* a = new B;
a->fun();
return 0;
}
初始化参数列表
C++的初始化参数列表是指在类的构造函数中使用冒号(:)后面跟着的一系列参数。它用于在创建对象时对成员变量进行初始化。首先介绍一下初始化参数列表的特点:
- 只能在构造函数里使用该语法,可以给所有成员设置初始化参数。
- 成员的构造顺序和在初始化参数列表中的顺序无关,与在类中声明顺序有关。
- const类型和引用类型在初始化参数列表中初始化。
下面看一下代码:
#include<iostream>
using namespace std;
class A {
int a;
int b;
int c;
const int d;
int& e;
public:
A(int a1, int b1, int c1, int d1) : a(a1), b(b1), c(c1), d(d1), e(a) {
cout << a << ' ' << b << ' ' << c <<' '<< d << ' ' << e << ' '<<endl;
}
};
int main() {
A a(1, 2, 3, 4);
return 0;
}
静态成员变量&&静态成员函数
静态成员变量
在定义类中的成员变量时,如果我们想保持这个成员的独立性,我们可以用static关键字将其定义为静态的,这就意味着不论创建多少个成员变量,该静态成员只有一个。
与静态变量相似,静态成员变量只能初始化一次,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
静态变量初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为0,。全局数据区的变量都有默认的初始值0,而堆区、栈区变量的默认值是不确定的,系统会给这个变量自动赋一个随机的值。
- 静态成员变量属于整个类所有,所有对象共享类的静态成员变量
- 静态成员变量需要在类外初始化(必须初始化,可以不赋值例如 int A::a = 1)
- 可以通过类名直接访问共有静态成员变量
- 可以通过对象名访问共有静态成员变量
- 静态成员变量在程序内部位于全局数据区(静态区)
- 静态成员变量的生命期不依赖于任何对象,与程序的生命周期一致
- 静态成员变量不会被继承,父类和子类共享静态成员变量
- 静态成员不会影响类的大小
#include<iostream>
using namespace std;
class A {/*类在刚创建的时候系统会提供一个字节大小的占位符 即sizeof(一个空的类) = 1*/
public:
static int a;
};
int A::a = 1;
int main() {
A a, b, c;
a.a++;
b.a++;
c.a++;
A::a++;
cout << a.a << endl;/* 5 */
return 0;
}
静态成员函数
- 定义静态成员函数,直接使用static关键字修饰即可,静态成员函数属于整个类所有,没有this指针
- 静态成员函数智能直接访问静态成员变量和静态成员函数(没有this指针,访问普通的成员变量和成员函数中都会自动把this指针隐藏)和类外部的其他元素。
- 可以通过类名直接访问静态成员函数
- 可以通过对象名访问类的共有静态成员函数
#include<iostream>
using namespace std;
class A {
int a;
public:
void work() {//this会当做隐藏参数传进
a = 1;//省略了this this->a = 1
}
static int b;
static void fun() {
//静态函数没有this指针 属于整个类 不属于某个对象
//静态成员函数可调用静态成员函数
//静态成员函数可以调用静态成员变量
//可以通过类名+作用域直接访问类的共有静态函数
//可以通过对象直接访问共有静态函数
//a=3 错误 因为非静态成员变量需要创建成员对象的时候才分配空间 而静态成员变量在创建成员对象之前就已经存在了 有冲突
b++;
}
};
int A::b = 1;//静态成员变量在类外初始化
int main() {
A a, b, c;
a.fun();
b.fun();
c.fun();
A::fun();
cout << a.b << endl;
}
友元
类的友元是定义在类外部,但有权访问类的所有私有成员,友元可以是一个函数,该函数被称为友元函数,友元也可以是一个类,被称为友元类。声明一个友元只需要在类中定义的类或者函数前加一个关键字friend。
#include<iostream>/*友元是定义在类的外部 但是有权限访问类的私有成员 友元可以是一个函数 可以使一个类 声明友元只需要在类定义声
明一个函数或者一个类在前面添加friend*/
using namespace std;
class A {
int nums = 1;
public:
friend void fun();//友元函数
friend class B;//友元类
};
void fun() {//友元函数能直接访问所属类中的私有变量
A a;
cout << a.nums << endl;
}
class B {//友元类能直接访问所属类中的私有变量
public:
void fun(){
A a;
a.nums = 5;
cout << a.nums << endl;
}
};
int main() {
fun();
B b;
b.fun();
return 0;
}
友元并不与面向对象的编程思想相悖,相反提高了公有接口的灵活性。友元形成的过程:任何函数,成员函数或者类想成为某个类的友元,是这个类来决定的(在变量前加关键字friend),而不是从外部强加友元。
1.为什么要使用友元函数?
为了使其它类的成员函数直接访问该类的私有变量。即:允许外面的类或者函数去访问类的私有变量和保护变量,从而使两个类共享同一函数或者变量。
2.使用友元函数的优缺点?
优点:提高效率,表达简单,清晰。
缺点:友元函数破坏了封装机制,尽量不使用。
常成员函数
格式:返回值 函数名(形参表) const {函数体} 例如:void fun(int a) const { }
特点:
- 可以读数据成员(只读),不能进行修改,对函数的功能有更明确的确定
- 常成员函数不能用来更新类的任何成员变量,也不能调用类中未用const修饰的成员函数,智能用来调用常成员函数。即常成员函数不能修改类中的成员状态,与const语义相符合
- 常函数能修改传入自身的形参以及内部定义的局部变量
- 常对象只能调用常函数,不能调用普通函数
#include<iostream>
using namespace std;
class A {
public:
int a;
int b;
mutable int c;
static void fun() {}
void fun1() const {//常函数不能修改成员变量,常函数中成员变量是只读的
cout << this->a << endl;
c = 5;
}
void fun1() {}//常函数可以和非常函数发生重载
void work() {}//常函数不能调用类内的非常函数,因为非常函数有this指针可以修改成员变量
void fun2(int a);
};
void A::fun2(int a) {
a = 5;//常函数中的参数可以修改
this->c = 4;//如果要在常函数中修改成员变量,需要在该成员变量前加上mutable,常对象对成员变量修改同理
}
int main() {
const A a;//常对象不能调用普通的成员函数,可以调用静态成员函数(因为静态成员函数没有this指针,不能修改成员变量)
a.fun1();
a.fun();//常函数和非常函数发生重载的时候,常对象默认调用常函数
//a.work();常对象不能调用非常函数
}
值得注意的是:
- 常对象不能调用非常函数,但是可以调用静态成员函数,也可以调用常函数。因为静态成员函数(没有this指针)和常函数都不能修改成员变量
- 常对象或者常函数需要修改成员变量 需要在成员变量前加一个关键字mutable
- 常函数和非常函数可以发生重载,常对象在调用的时候默认调用常函数,非常对象在调用的时候默认调用非常函数,非常对象可以调用常函数
- 函数声明和实现分离时 const是函数声明的一部分,在函数的实现部分也要加上const,也就是在类内声明 void fun(int a) const;在类外实现就应该写成void A :: fun(int a) const{}
- 常函数和常函数之间可以发生重载
意义:
在实现只读作用的函数中,可以防止程序员不小心修改了成员变量的值
拷贝构造函数
使用场景:
- 在用一个对象创建一个新的对象时会采用拷贝构造函数
- 对象作为函数的返回值以值的方式从函数返回
对象作为函数参数,以值的传递方式传给函数
#include<iostream>
using namespace std;
/*
* 使用场景:
1.使用一个已经存在的对象创建一个新的对象
2.对象作为函数的返回值以值的方式从函数返回
3.对象作为函数参数,以值的传递方式传给函数
*/
/*
5点
1.拷贝构造的使用场景
2.匿名对象
3.拷贝构造的参数为什么要用引用
4.深拷贝和浅拷贝
5.关键字explicit
*/
class A {
int num;
public:
A(int num) {
this->num = num;
}
A(const A& other) {//当构造函数前加一个关键字‘explicit’时 就不能用A b = a,就应该写成A b(a);
//加一个&参数变成引用类型 此时不会发生拷贝 避免先入拷贝构造循环
this->num = other.num;
cout << "拷贝构造" << endl;
}
~A() { cout << "析构函数" << endl; }
};
void fun(A a) {}
A fun() {
A a(3);
return a;//返回时运用拷贝构造 如果没有值接收 则创建一个匿名对象 接收
}
int main() {
A(2);//创建了一个匿名对象,匿名对象只存在当前行,当前行结束就销毁,即析构
cout << 1 << endl;
A a(3);
A b = a;//编译器内部转换成A b(a);的格式
A c = 2;//编译器内部转换成A c(2);的格式
fun(a);
A d = fun();
return 0;
}
深拷贝和浅拷贝
- 对象中含有指针类型的成员变量时需要用深拷贝构造,否则用浅拷贝构造
- 编译器默认的拷贝构造函数是浅拷贝构造
- 如果对象中含有指针变量确实用了前拷贝构造,那么会导致两个指针变量指向同一块地址空间被释放两次,编译器报错
- 浅拷贝和深拷贝的区别在于两个指针变量指向的是一块空间还是指向不同的空间,如果没有创建内存的操作就是浅拷贝,否则是深拷贝。
#include<iostream>
using namespace std;
/*
对象中含有指针类型的成员变量时需要深拷贝构造,否则浅拷贝
编译器默认的拷贝构造函数是浅拷贝构造函数
如果对象中含有指针变量却使用了浅拷贝构造,那么会导致两个指针变量指向同一块地址空间,那么在对象释放时会导致一块空间释放两次,编译器报错
浅拷贝和深拷贝的区别在于两个指针变量指向的是一块空间还是指向不同的空间,如果没有创建内存的操作就是浅拷贝否则是深拷贝
*/
class Test {
int num;
int* p = nullptr;
public:
Test() { cout << "无参构造" << endl; }
Test(int n) {
cout << "有参构造" << endl;
num = n;
if (num > 0)p = (int*)malloc(sizeof(int) * num);//或者p = new int*[num]
}
Test(const Test& other) {
cout << "拷贝构造" << endl;
this->num = other.num;
//if (other.p) this->p = other.p;浅拷贝
if (num > 0) {
this->p = (int*)malloc(sizeof(int) * num);
}
}
~Test() {
if (p) {
free(p);
p = nullptr;
}
cout << "析构函数" << endl;
}
};
Test fun() {
Test t;
return t;
}
int main() {
Test t(3);
Test t1(t);
return 0;
}
-
有几点需要注意:
-
返回值是以值的形式返回时,在函数调用部分如果有对象接收会通过拷贝构造传递给对象,如果没有对象接收 也会拷贝一个匿名对象,但是这个匿名对象只存在于当前行
-
拷贝构造的参数需要引用,因为如果不是引用会陷入拷贝构造的循环中
-
如果对象中含有指针类型的成员变量,则需要用到深拷贝构造,否则前拷贝构造
-
如果在拷贝构造函数前加入一个关键字explicit则不能使用隐式调用
例如以下代码:
class A{
int a;
public :
A(int a){this->a = a;}
A(const A& other){this->a = other.a;}
//explicit A(const A& other){this->a = other.a;} 这种情况不能隐式调用
}
int main(){
A a(5);
A b = a;//隐式调用相当于A b(a);
return 0;
}
运算符重载
我们知道C++的运算符例如:‘+’、‘-’、‘ * ’、‘ / ’等都是仅限于基本的内置数据类型(char,int,float,double等),但是对于我们自定义的类型却不能直接使用运算符,但是在以特殊情况下我们会遇到要使用运算符实现自定义类型之间的运算,那么我们就要用到——运算符重载。
C++运算符重载实质上就是函数重载,运算符重载实质上是C++的一种多态,叫做静态多态,先前我们学到的动态多态是父类指针指向子类对象(也就是在编译时才能确定的动态多态),而静态多态是在编译之前就已经确定的。目的是为了让我们使用同名的函数来完成不同的操作。
#include<iostream>
using namespace std;
class Box {
public:
int length;
int width;
int height;
//如果成员变量是私有private 使用友元friend ostream& operator<<(ostream& o, const Box a);
Box() {
length = 0;
width = 0;
height = 0;
}
Box(int length, int width, int hight) {
this->length = length;
this->width = width;
this->height = height;
}
Box operator+(const Box& other) {
Box o;
o.length = this->length + other.length;
o.width = this->width + other.width;
o.height = this->height + other.height;
return o;
}
Box operator++() {//括号内无int 参数占位符 前++
this->length += 1;
this->width += 1;
this->height += 1;
return *this;//this指针,
}
Box operator++(int) {//括号内有int参数占位符 后++
Box p = *this;//定义一个变量记录++之前的数据
this->length += 1;
this->width += 1;
this->height += 1;
return p;//返回++之前的状态
}
void operator=(const Box& other) {
this->length = other.length;
this->width = other.width;
this->height = other.height;
}
bool operator>(const Box& other) {
return this->height > other.height;
}
};
ostream& operator<<(ostream& o, const Box a) {//必须在类外,如果成员变量是私有private的 使用友元
o<< a.height << " " << a.width << " " << a.height << endl;
return o;
}
int main() {
Box a, b;
Box q = a + b;
a = b;
cout << q;
cout << q++;
cout << ++q;
int a1 = 1;
cout << a1++ << ++a1 << a1++ << a1++;//根据参数入栈顺序: a++,a++,++a,a++
a++:a' = 1,a = 2; 输出a'
a++:a' = 2,a = 3; 输出a'
++a:a = 4;
a++:a' = 4,a = 5,输出a',程序完事之后 ++a输出a = 5
输出4521
}
有几点要注意的是
在上述代码中a++返回的是a',而++a返回的是a,根据这个原理我们可计算上述表达式。
- 后++运算符在运算符重载函数形参部分需要写入一个int参数占位符来代表此函数为后++。
- 写出‘cout’ 是库函数 ‘ostream’ 中的一个对象,用于标准输出。它是 C++ 的输入/输出流库中最常用的对象之一。写入‘cin’是库函数‘istream’中的一个对象
- 在最后的部分中我们要注意函数入栈出栈的顺序,避免出现错误
实现string类
创建一个string类通常会在面试中出现,一个string类通常会有头文件:string.h 主函数文件:main.cpp string文件:string.cpp。
Mystring.h:
#pragma once
#include<iostream>
using namespace std;
class MyString {
private:
char* data;//字符串的首地址
int size;//字符串的长度,不算'\0'
public:
friend ostream& operator<<(ostream& o, MyString s);
MyString();//默认构造
MyString(int n, char c);//构造了n个字符c的字符串
MyString(const char* source);//利用const char* 构造
MyString(const MyString& other);//拷贝构造
~MyString();
char operator[](int i);
char operator[](int i)const;
MyString& operator = (MyString& other);
bool operator == (MyString& other);
MyString& operator+=(const MyString& other);
bool operator>(const MyString& other);
};
Main.cpp:
#include"Mystring.h"
int main() {
MyString s(1, 'c');
return 0;
}
Mystring.cpp:
#include"Mystring.h"
ostream& operator<<(ostream& o, MyString s) {//输出
for (int i = 0; i < s.size; i++) {
o << s[i];
}
return o;
}
MyString::MyString() {//无参构造
size = 0;
data = new char[1];
*data = '\0';
}
MyString::MyString(int n, char c) {//创建一个长度为n的Mystring类型的对象
size = n;
data = new char[n + 1];
char* temp = data;
while (n--) {
*temp++ = c;
}
*temp = '\0';
}
MyString::MyString(const char* source) {//用一个char类型的对象创建一个和source相同的Mystring类型的对象
if (source == nullptr){
size = 0;
data = new char[1];
*data = '\0';
}
else {
size = strlen(source);
data = new char[size + 1];
strcpy_s(data, size + 1, source);
}
}
MyString::MyString(const MyString& other) {//用一个Mystring类型的对象创建一个和source相同的Mystring类型的对象 拷贝构造
size = other.size;
data = new char[size + 1];
strcpy_s(data, size + 1, other.data);
}
MyString::~MyString() {//析构
cout << "--------析构函数---------" << endl;
if (data != nullptr) {
delete[]data;
data = NULL;
size = 0;
}
}
char MyString::operator[](int i) {//下表访问 非常量 可修改
return this->data[i];
}
char MyString::operator[](int i)const {//下表访问 常量 只读 不可修改
return this->data[i];
}
MyString& MyString::operator = (MyString& other) {//赋值
cout << "-------调用了赋值运算符-------" << endl;
if (data != nullptr)delete[] data;
if (other == *this)return *this;
size = other.size;
data = new char[size + 1];
strcpy_s(data, size + 1, other.data);
return *this;
}
bool MyString::operator == (MyString& other) {//判断是否相等
if (other.size != size)return false;
char* p = other.data;
char* q = data;
while (*q && *p) {
if (*p != *q)return false;
q++;
p++;
}
return true;
}
MyString& MyString::operator+=(const MyString& other) {//连接 this 和 other
if (other.size == 0)return *this;
int len = size + other.size;
char* temp = data;
data = new char[len + 1];
strcpy_s(data, size + 1, temp);
strcat_s(data, len + 1, other.data);
delete[] temp;
size = len;
return *this;
}
bool MyString::operator>(const MyString& other) {//判断大小:第一个不同的字符
int i = 0;
while (other.data[i] == data[i] && data[i] != '\0' && other[i] != '\0') {
i++;
}
return data[i] > other.data[i]?true : false;
}
在此基础上还可以进行拓展其他操作。实现string类主要用到的就是上一节讲到的运算符重载。
内联函数
首先我们先了解一下函数的调用规则以及内存分区:
上图是函数调用的一个流程图,在我们程序运行到函数调用部分的时候,我们需要执行函数,而函数需要在栈区开辟空间,这一操作需要花费时间和空间,在执行完函数之后,需要返回到函数调用的部分,这一行为需要采取"寻址"这一操作,同时,也花费了时间。
再说说内存分区:
我们的内存地址存储顺序为从常量区到内核的地址是逐渐递增的方式,在栈区中相反,我们常说的代码区就是在常量区中。
宏函数
在介绍内联函数之前,我们先来回顾学习一下宏,什么是宏呢?
- 宏函数,定义常量
- 本质:文本替换
- 特点:速度快,直接进行文本替换,相当于直接将函数中的东西复制过来,也就相当于调用部分不是一个函数免去了时间消耗,但是由于需要把调用部分换成宏函数中的代码,但是宏函数中的代码需要在常量区中的代码区开辟空间,所以花费了空间。省去了开辟函数栈以及寻址的时间。
下面我们来看一组代码:
#include<iostream>
using namespace std;
#define ADD(x,y) ((x) + (y))
#define ADD_One(x,y) (x * y);
int main() {
cout << ADD("asd", 1) << endl;//宏定义没有类型检查
cout << ADD_One(1 + 2, 3 + 4);//宏定义有二义性
}
执行结果:
下面我们来解释一下为什么出现这种结果:
- 输出结果为sd的原因:"asd"为字符串首地址也就是字符‘a’的地址,a的地址加一就是字符串"sd"的地址,所以输出"sd",由此可以看出宏定义没有类型检查。
- 再看第二个输出结果"11":将代码带入到宏函数中得出的式子为1 + 2 * 3 + 4计算出的结果就为11,由此可以看出宏定义有二义性。
ps:二义性:一个语句有多种结果。
为了避免出现这种类似的我们不想看到的结果,通常采用——内联函数。
如图:当发生函数中的代码运行所需的时间小于开辟函数以及函数返回寻址所需的时间时,我们可以采用内联函数。
内联函数的定义:
以“inline”修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升了程序运行的效率。
如果一个函数本身就是几十行甚至上百行,那么函数本身执行所需要的时间就会很大,则调用函数,创建栈帧(每一个栈帧都对应着一个未运行完的函数,栈帧中保存了该函数的返回地址以及函数中的局部变量,我们也可以理解为栈帧就是函数运行时所开辟的函数栈)所需的时间可以忽略不计;但若是一个函数本身就一两行,运行函数的时间与调用函数本身以及函数返回寻址所需的时间相比可以忽略不计的话,宏函数或者内联函数的提前替换就显得格外优秀,提高了运行效率。
#include <iostream>//类内的内联函数如果在内类声明类外实现 类内和类外必须都得在返回值类型前加 inline
using namespace std;
inline int add(int a, int b) {
return a + b;
}
int main() {
cout << add(1 + 2, 3 + 5) << endl;
}
内联函数的特点:
- inline是一种以空间换时间的做法,省去了调用函数,建立栈帧的额外开销,但如果函数里的代码很长,或者有递归函数,因为我们的编译器很智能可以选择最优的方式来判断是否将其定义为内联函数,若函数本身消耗时间就很大,即使在函数前面声明了inline,编译器也不会让该函数成为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,会根据函数所需时间判断是否要将该函数定义为内联函数
- inline不可以声明和定义分离,在.h头文件中使用inline声明内联函数则.cpp文件中不适用inlin定义函数的话,会报错连接错误
模板
接下来我们简单介绍一下什么是模板:
首先有两个关键字:
- template:定义模板的关键字
- typename:定义模板参数
模板函数
#include<iostream>
using namespace std;
template <typename T>
int compare(T& t1, T& t2) {
if (t1 > t2)return 1;
if (t1 == t2)return 0;
if (t1 < t2)return -1;
}
代码中的T的类型与传给函数的参数的类型相同。
模板类
我们通过一个实现栈来介绍模板类
#include<iostream>
#include<vector>
using namespace std;
template<typename T>
class stack {
vector<T> vec;
public:
void pop() {
vec.pop_back();
}
T top() {
return vec.back();
}
void push(T a) {
vec.push_back(a);
}
bool empty() {
return vec.size() == 0 ? 1 : 0;
}
};
int main() {
stack<int> s1;//必须要写上类型 <int>
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
while (!s1.empty()) {
cout << s1.top() << endl;
s1.top();
}
}
值得注意的是我们所实现的栈是基于vector数组来实现的,而模板中的数组中的类型需要显示指定即:stack类中vector<T> vec对应主函数中stack<int> s1。
异常
在学习异常之前,我们先来举一个例子:假设有个数组vec[5],不难看出,数组中有5个元素,当我们访问vec[9]的时候,系统会提示错误——访问越界,程序就会停止,那么下面的代码也就不会执行到了。
那么如果我们面临一个项目来说,这个项目中有一个小的功能出错了,如果这个错误不影响其他功能,那么我们可以选择——异常
用一种简单易懂的方式形容异常就是:这个函数throw(抛出)了一个异常对象,那么紧接着下面会捕获这个异常对象,如果捕获到了这个异常对象,那么会发出一些信息来提醒这个部分出现了异常,如果没有捕获到异常对象,则不会采取任何操作,这样不管是否有没有异常,下面的代码还是会正常运行。
异常体系
首先我们先来介绍一下C++标准库中的异常体系:
exception是所有异常类型的基类,我们可以用exception类型接收所有异常类型,来实现父类指针或引用指向子类对象,从而实现多态。
下面我们来看一个异常举例:
#include<iostream>
using namespace std;
#include<vector>
//exception 类型是所有异常类型的 基类,可以用exception类型的指针或者引用来指向其他异常类型对象,来实现多态
//try抛出异常 catch接受异常 接受异常之后程序继续进行 异常继续
int main() {
vector<int> vec;
try {
vec.at(4);
}
catch (out_of_range& e) {
cout << '1' << endl;
cout <<e.what() << endl; //invalid vector subscript
}
catch (exception& e) { //当存在两个catch时执行第一个,当用exception异常类型时,实现多态
cout << '2' << endl;
cout << e.what() << endl;
}
cout << "异常继续" << endl;
return 0;
}
下面来解释一下上述代码:vector中的at函数就是检测括号中的数值是否是越界的,如果越界就会抛出out_of_range异常信号,上述代码中抛出out_of_range异常信号之后,下面的第一个catch函数中的参数为out_of_range类型的引用,则会执行catch中的代码;而第二个catch函数中的参数为exception类型的引用,exception类型是所有异常类型的父类,则在代码e.what()中会实现多态——重写。当异常被一个catch接收到之后,下面的catch就不会接收了,下面的代码会正常进行。
异常类
下面我们来实现一个异常类:
#include<iostream>
#include<string>
using namespace std;
class MyException {
private:
string msg;
public:
MyException(string str) :msg(str) {}
MyException(const MyException& other) {
this->msg = other.msg;
cout << "拷贝构造函数" << endl;
}
void what() {
cout << msg << endl;
}
};
int fun(int a, int b) {
if (b == 0) {
MyException m("-----异常:除数不能为0-----");//创建对象
throw m;
}
cout << "函数抛出异常就会被结束" << endl;
return a / b;
}
int main() {
try {
fun(1, 0);
}
catch (MyException& e) {
e.what();
}
cout << "程序继续执行没有被终止" << endl;
return 0;
}
下面是代码流程图
单例模式
单例模式也称单件模式,是使用最广泛的设计模式之一,简单说明一下就是这个类只允许创建一个对象(实例);其意图就是保证一个类仅有一个实例,并提供一个访问它的全局访问点(公有的静态函数),该实例被所有程序模块共享。
单例模式有两种:懒汉版,饿汉版
首先简述一下单例类的特点:
- 构造函数私有化:防止外界再创造对象
- 使用静态指针变量指向这个唯一的对象(实例)
- 使用一个公有静态方法(函数)获取该对象(实例)
懒汉模式
//懒汉
class singketon {
static singketon* instance;
singketon() {};
singketon(const singketon&);
public:
static singketon* get() {
if (instance == nullptr)
instance = new singketon();
return instance;
}
};
singketon* singketon::instance = NULL;//初始化为空
懒汉模式就是在外界调用静态函数的时候在创建对象,指针变量,构造函数,拷贝构造均为私有化,静态函数公有化,以便其他地方调用。
饿汉模式
//饿汉
class singketon {
static singketon* instance;
singketon() {};
singketon(const singketon&);
public:
static singketon* get() {
return instance;
}
};
singketon* singketon::instance = new singketon();//初始化为空
饿汉模式可以理解为等不及了,在静态函数没有被调用的时候就已经创建好对象了。
综上所述,单例模式就是只有一个该类型的变量,不允许再创建其他的变量,而懒汉模式和饿汉模式的区别就在于:懒汉模式是在调用函数之后才创建对象,饿汉模式是在调用函数之前就已经把对象创建好了。
lambda表达式
我们来介绍一下lambda表达式
- lambda表达式可以就地匿名定义目标函数或函数对象,不需要额外写一个函数
- lambda是一个匿名的内联函数
lambda表达式定义了一个匿名函数,语法如下
捕获列表:捕获一定范围内的变量
参数列表:和普通的参数列表一样,没有参数可以省略不写
#include<iostream>
#include<vector>
using namespace std;
//lambda可以就地匿名定义目标函数或函数对象,不需要额外写一个函数
//lambda是一个匿名的内联函数
//[ capture ]( params ) -> ret{body;};
//捕获列表[]:捕获一定范围的变量,参数列表():和普通函数的参数列表一样,如果没有参数参数列表可以不写
//auto fun = [](){} auto fun = []{}
//捕获列表 []不捕获任何变量 [&]捕获外部作用域中所有变量,并且按照引用捕获
//[=]捕获外部作用域的所有变量,按照值捕获,拷贝过来的副本在函数体内是只读的
//[=,&a]按值捕获外部作用域中所有变量,并且按照引用捕获外部变量a
//[b]按值捕获b变量,只读
//[this]捕获当前类中的this指针,让lambda表达式拥有当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限
class A {
public:
int a = 10;
void output(int x, int y) {
auto f1 = [this] {a++; };
//auto f2 = [this]{return a += x;};//错误 只捕获了当前类中的
auto f3 = [=] {return a + x + y; };
auto f4 = [this, x, y] {return a + x + y; };
}
};
int main() {
int a = 10, b = 20;
//auto f1 = [] {return a; }; 错误 因为没有捕获任何外部变量
auto f2 = [&] {return a++; }; //正确 按照引用捕获外部所有变量
auto f3 = [=] {return a; }; //正确 按照值捕获外部所有变量
//auto f4 = [=] {return a++; };错误 按照值捕获所有外部变量 只读
//auto f5 = [a] {return a + b; };错误 只按照值捕获了a变量
auto f6 = [a, &b] {return a + (b++); };//正确 按照值捕获a 只读a,按照引用捕获b 所以可以修改
auto f7 = [=, &b] {return a + (b++); };//正确 按照值捕获外部所有变量,并且按照引用捕获了b
auto f = [](int i) {return i; };
//auto fun1 = []() {return { 1,2 }; };错误 基于列表初始化需要明确声明函数的返回值类型
auto fun1 = []()-> vector<int> {return { 1,2 }; };
return 0;
}
lambda用法
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
//int main() {
// vector<int> vec = { 1,2,3,4,5,6 };
// sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });
// for (auto it : vec) {
// cout << it << " ";
// }
//
//}
vector<int> nums;
vector<int> largeNums;
const int ubound = 10;
inline void LargeNumsFunc(int i) {
if (i > ubound) largeNums.push_back(i);
}
void Above() {
//传统的for循环
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it >= ubound)
largeNums.push_back(*it);
}
//使用函数指针
for_each(nums.begin(), nums.end(), LargeNumsFunc);
//使用lambda和算法for_each
for_each(nums.begin(), nums.end(), [=](int i) {if (i >= ubound) largeNums.push_back(i); });
}
值得注意的是lambda表达式本身就是一个函数指针,在比较器中可直接写入。