• C++11 基础知识


    1、命名空间

    引入主要是为了解决重定义问题,系统中可以定义多个命名空间,命名空间可以定义在多个文件,如果以往没有出现就是定义一个命名空间,如果已经出现过,就代表打开定义的命名空间

    namespace 命名空间名字
    {
    void way()
    {
    }
    }
    
    // 访问命名空间内容:使用作用域运算符::
    命名空间名字::way();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2、auto关键字

    auto自动类型推断,可以在声明变量的时候,根据变量初始值,匹配类型(声明时要赋予初值),发生在编译阶段,不会影响程序运行效率

    auto b = true;
    auto ch = "hello world";
    
    • 1
    • 2

    3、头文件防护

    条件编译

    // 如果没有定义标识符会进入,执行程序段
    #ifndef 标识符
    程序段
    #endif
    
    • 1
    • 2
    • 3
    • 4

    4、引用

    为变量起,别名,起完后看成时同一个变量,别名变量和原变量占用同一块内存,定义引用必须初始化

    int value = 10;
    int &bak = value;
    
    • 1
    • 2

    5、常量

    const表示不变的意思

    const int var = 7;
    // 也可以通过引用进行修改
    int &var2 = (int&)var;
    var2 = 18;
    
    • 1
    • 2
    • 3
    • 4

    constexpr关键字:c++11引入,它也是常量概念,在编译时求值,能提升性能,

    constexpr int var = 1;
    
    
    • 1
    • 2

    6、for

    int v[]{1,2,3,4,5,6,7};
    // 依次将数组v中每个元素拷贝到x中进行打印
    for(auto x:v)
    {
    	cout<<x<<endl;
    }
    // 优化,省了拷贝
    for(auto &x:v)
    {
    	cout<<x<<endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    7、new

    c++内存分为5个存储区:
    1、栈:一般函数局部变量存放在这里,由编译器自动分配释放
    2、堆:程序员用malloc、new,用free和delete释放,忘记释放,系统回收
    3、全局/静态存储区:放全局变量和static变量,程序结束系统释放
    4、常量存储区
    5、代码区
    栈空间是有限的,分配速度快
    堆只要不超出实际物理内存,也在操作系统允许你能够分配的大小,分配速度慢

    malloc和free是函数,
    newdelete不是函数,是运算符,会调用构造函数和析构函数
    指针变量名 = new 类型标识符
    指针变量名 = new 类型标识符(初始值)表示初始值
    指针变量名 = new 类型标识符[内存单元个数]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    8、nullptr

    代表也是空指针,使用nullptr是为了避免指针和整数发生混淆

    9、函数后置返回类型

    函数声明
    auto fun(int a)-> void;
    
    • 1
    • 2

    10、内联函数

    内联函数与普通函数不同,其仅仅在本文件内可见,而不是全局可见的。
    1、inline 函数和普通函数链接的区别?
    普通函数是整个工程可见的,可以跨文件链接,而 inline 函数仅仅当前文件可见,不可以跨文件链接。
    2、inline 函数的定义必须写在头文件中吗?
    不必须。但是如果不写在头文件中,那么在每一个调用了 inline 函数的文件中都必须有该 inline 函数的定义,且每个文件中定义可以不同,相当于不同文件中的同名 inline 函数实际上不是同一个函数。
    3、为什么类中定义的函数会被编译器自动判定为 inline 函数?
    因为在工程中,类的定义通常写在某个头文件中,任何想要使用该类的源文件都需要先包括该头文件,如果在类中定义的函数不是 inline 函数,则有可能出现多重定义的问题。比如说有一个头文件定义了一个类,且该类中有一个实现在类里面的成员函数,工程中有两个源文件都包含了该头文件,那么经过预处理后,相当于在两个源文件中都有对该成员函数的定义,如果该成员函数没有被编译器判定为 inline 函数,则在链接的时候会出现多重定义的错误。
    4、原理是什么?
    因为 inline 函数在编译的时候就被插入到调用处,编译器不会单独为一个 inline 函数生成汇编代码,而是在调用的地方直接生成汇编代码插入到调用处,这个是属于编译阶段的事情而不是链接阶段的事情,所以在编译的代码生成阶段就需要拿到 inline 函数的定义。如果编译器在编译的代码生成阶段没有拿到 inline 函数的定义,则将对其的调用推迟到链接时,但是由于对于 inline 函数的定义处,编译器并未生成汇编代码,所以会链接失败

    在函数定义前添加inline,函数就会变成内联函数
    函数体小,调用频繁,在编译阶段对inline函数进行处理,系统尝试将调用的动作替换成函数本体,通过这种方式提高性能。
    inline void fun(int a)
    {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    11、const char* 、char const*、const char* const 区别

    1const char* p;
    p指向的地址可以改变,p指向的内容不能修改
    
    2char const* p;
    p指向的内容可以修改,p指向的地址不能修改
    
    3const char* const
    p指向不能改变,内容不能改变,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    12、迭代器

    在这里插入图片描述

    迭代器是遍历容器的一种数据类型,类似指针,迭代器用来指向容器中某个元素

    	vector<int> vi = { 100,200,300 };
    	vector<int>::iterator beginiter,endIter;
    	beginiter = vi.begin();	// 如果容器有元素返回第一个元素
    	endIter = vi.end();	// 返回容器末尾后边
    	// 正向迭代器
    	for (vector<int>::iterator iter = vi.begin(); iter != vi.end(); iter++)
    	{
    		cout << *iter<<endl;
    	}
    
    	// 反向迭代器
    	for (vector<int>::reverse_iterator riter = vi.rbegin(); riter != vi.rend(); riter++)
    	{
    		cout << *riter<<endl;
    	}
    
    	// 常量迭代器,不能修改容器的值
    	for (vector<int>::const_iterator citer = vi.cbegin(); citer != vi.cend(); citer++)
    	{
    		cout << *citer<<endl;
    	}
    
    	while (!vi.empty())
    	{
    		auto iter = vi.begin();
    		vi.erase(iter);
    	}
    	
    	if (vi.empty())
    	{
    		cout << "容器为空" << endl;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    小案例:

    struct person
    {
    	int id;
    	char name[64];
    };
    
    int iSequal(vector<person*>& vTmp, char* str)
    {
    	for (auto iter = vTmp.begin(); iter != vTmp.end(); iter++)
    	{
    		if (strcmp(((*iter)->name), str) == 0)
    		{
    			return 1;
    		}
    	}
    	return 0;
    }
    
    int main()
    {
    	vector<person*> vPerson;
    	for (int i = 0; i < 5; i++)
    	{
    		struct person* tmp = new person;
    		tmp->id = i + 1;
    		strcpy(tmp->name, "abc");
    		vPerson.push_back(tmp);
    	}
    	int result = iSequal(vPerson, "abc");
    	if (result)
    	{
    		cout << "相等" << endl;
    	}
    	else
    	{
    		cout << "不相等" << endl;
    	}
    
    	// 释放
    	for (auto iter = vPerson.begin(); iter != vPerson.end(); iter++)
    	{
    		if ((*iter) != NULL)
    		{
    			delete (*iter);
    			*iter = NULL;
    		}
    	}
    	vPerson.clear();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    13、类型转换

    1、隐式类型转换:系统自动进行,不需要介入
    2、显式类型转换(强制类型转换)
    static_case:静态转换,正常转换,编译时候进行类型转换检查和c语言强制转换相似
    dynamic_case:主要用于,运行时类型识别检查,主要用来,父类类型和子类型之间转换
    const_case:去除指针或引用的const属性,只能将const属性去掉,编译时进行转换
    reinterpret_case:编译时,进行类型转换,重新解释,将操作数内容解释为另一种类型,常用于如下两种:将整型转换成指针,一种类型指针转换另一种指针

    14、explicit

    如果构造函数声明带有explicit,则这个构造函数只能初始化和显示类型转换
    class Time
    {
    public:
    	Time(int a)
    	{
    	}
    };
    
    // 发生了隐式类型转换
    Time t = 11;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    15、成员函数末尾const

    需要在成员函数声明和定义中添加const
    作用:告诉系统,这个成员函数,不会修改该对象任何成员变量值,不会修改类的任何状态,常量成员函数

    class Time
    {
    public:
    	void add(int a,int b)const;
    	int m_a;
    };
    
    void Time::add(int a,int b)const
    {
    	m_a = a; //错误,不能修改
    }
    
    定义const对象,对象限制,只能调用const函数
    const Time time;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    16、mutable

    mutable为了突破const限制,用来修饰成员变量,一旦被修饰,就表示永远可被修改。

    class Time
    {
    public:
    	void add(int a,int b)const;
    	mutable int m_a;
    };
    
    void Time::add(int a,int b)const
    {
    	m_a = a; // 可以修改
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    17、返回自身this

    1、this只有在成员函数中使用,不能在静态函数和全局函数中使用
    2、this其实就是一个Time* const 常量指针,不能修改指向
    3、在const函数中this是const Time* const类型指针
    在调用函数时,系统默认重写了函数,在前面将类的地址传给了this

    class Time
    {
    public:
    	// 返回自身
    	Time& fun()
    	{
    		return *this;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    18、static成员变量、成员函数

    属于整个类的成员变量,不属于某一个实例
    特点:一旦在某个对象中修改了值,在其他对象中也可以看到结果,成员函数也是一样,调用:类名::成员变量名,静态成员函数只能操作静态成员变量

    在cpp文件开头定义静态成员变量,定义时候才分配内存,能保证任何对象正常使用
    int Time::mystatic=12;
    
    • 1
    • 2

    19、派生类对象定义调用构造函数顺序

    先调用父类构造函数,然后调用子类构造函数,先调用子类析构函数,然后调用父类析构函数
    在这里插入图片描述

    20、多态

    条件:
    1、继承
    2、重写父类虚函数
    3、父类指针或引用指向子类对象
    调用虚函数执行的是动态绑定,表示程序运行时才知道调用的是子类还是父类的函数

    21、override、final

    子类

    为了避免在子类中写错虚函数,在c++11中,你可以在子类函数声明后面添加override,虚函数专用

    父类

    final虚函数专用,是用于父类,如果父类函数声明后面加了final,那么任何尝试覆盖该函数的操作将引发错误

    22、基类析构函数写成虚函数

    要是使用父类指针指向子类对象,delete时不会调用子类析构函数,需要将父类析构函数写成,虚析构函数

    23、friend

    友元关系不能继承
    友元函数

    1、友元函数
    在类中声明友元函数,就可以访问类中的所有成员变量和成员函数了
    friend void func();

    友元类

    如果你是我的友元类,你就可以在你的成员函数中访问所有成员。
    声明友元类:
    friend class C;

    24、RTTI是什么

    运行时类型识别,通过运行时识别,程序能够使用基类的指针或引用来检查这些指针或者引用的对象实际派生类型
    1、dynamic_case运算符:能够将基类的指针或引用安全的转换为派生类的指针或引用
    2、typeid运算符:返回指针或引用所指向的实际类型
    想要RTTI两个运算符正常工作,基类至少需要有一个虚函数

    class A
    {
    };
    
    class B:public A
    {
    };
    
    A* a = new B;
    B* b = dynamic_case<B*>(a);
    // typeid返回值是一个常量引用,这个常量引用是一个标准库类型type_info
    cout<<typeid(*b).name()<<endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    25、虚函数表

    如果类中有虚函数,编译器就会对该类产生一个虚函数表,虚函数有很多项,每一项都是一个指针,指向的是这个类的各个虚函数的入口地址,虚函数表项的第一项,它指向的不是入口地址,它指向的type_info对象

    26、临时对象

    1、以传值的方式给函数传递参数
    2、类型转换生产的临时对象,隐式类型转换保证函数调用成功
    3、函数返回临时对象

    27、左值引用和右值引用

    引用分类:
    1、左值引用(绑定到左值)
    2、const 引用(常量引用)
    3、右值引用(绑定到右值)它是个引用

    1、左值引用
    int  value = 10;
    int &ret = value;
    
    2const引用
    const int &ret = value;
    
    3、右值引用
    int &&ret = 3;
    ret = 5;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    左值引用:引用左值,绑定左值上
    右值引用:就是引用右值,绑定右值,希望用右值引用来绑定一些即将销毁的或者是一些临时对象上。

    总结:
    1、返回左值引用函数,连同赋值,下标,解引用和前置递增递减运算,都是返回左值表达,
    我们可以将一个左值绑定到左值表达式
    2、返回非引用类型函数,连同算术,关系,以及后置递增运算符,都是生成右值,
    可以用const的左值引用或者右值引用绑定到这类表达式
    
    重点:
    int i = 1;
    int &&r1 = i++; // 绑定右值,但是r1和i没有关系
    (1)r1虽然是右值引用绑定到了右值,但是r1本身是左值,把r1看成一个变量
    int &r2 = r2; // r1是左值 所以可以绑定
    (2)所有变量都是左值,因为他们有地址,
    (3)任何函数形参都是左值
    (4)临时对象都是右值
    
    右值引入目的:
    c++11引入新概念,&&代表一种新的数据类型,提高程序运行效率,把拷贝对象,变成移动对象
    
    std::move函数:
    把左值,强制转换成右值,右值引用可以进行绑定
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    28、对象移动的概念

    A移动B,那么A对象我们就不能使用了
    移动:并不是内存中数据移动到另一个地址

    拷贝构造函数:
    Time::Time(const Time& tmp);
    
    移动构造函数
    Time::Time(const Time&& tmp);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    移动构造函数和移动赋值运算符完成的功能
    1、完成必要的内存移动,斩断原对象和内存的关系
    2、确保移动后原对象处于一种,即使销毁也没有什么问题,不在使用原对象。

    演示

    
    class A
    {
    public:
    };
    class B
    {
    public:
    	B():m_b(100)
    	{
    	}
    	B(const B& tmp):m_a(new A(*(tmp.m_a)))
    	{
    		m_b = tmp.m_b;
    	}
    	// noexcept通知标准库,移动构造函数不抛出异常,提高效率
    	B(B&& tmp)noexcept :m_a(tmp.m_a)
    	{
    		m_a = nullptr;
    	}
    	virtual ~B()
    	{
    		if(m_a!=NULL)
    		{
    			delete m_a;
    			m_a = NULL;
    		}
    	}
    	int m_b;
    	A* m_a;
    };
    
    static B getB()
    {
    	B b;
    	return b; // 临时对象,调用拷贝构造函数
    	// 如果对象B有移动构造函数会自动调用
    }
    
    int main()
    {
    	B b = getB();
    	B b1 = std::move(b); // 建立新对象,调用移动构造函数
    	B&& b2 = std::move(b);// 这里没有创建新对象,根本不会调用移动构造函数
    	//效果等同于,b2是b的新别名
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    29、继承构造函数

    class A
    {
    public:
    	A(int a,int b,int c):m_a(a),m_b(b),m_c(c)
    	{
    	}
    public:
    	int m_a;
    	int m_b;
    	int m_c;
    };
    
    class B:public A
    {
    public:
    	using A::A; // 继承A构造函数,using就让某个名字在当前作用域内可见,
    	// 遇到这条代码时候,基类的每个构造函数,都会生产一个与之对应的派生类构造函数,
    	//B(构造函数形参列表):A(构造函数形参列表){}函数为空
    	// 如果A类构造函数有默认参数
    	// using A::A 会生产两个构造函数
    	// 1、一个是带有所有参数的构造函数
    	// 2、省略掉默认参数的那一个
    }
    
    int main()
    {
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    虚继承(virtual base class)

    虚继承,虚继承(派生类)
    派生列表中,同一个基类只能出现一次,但是如果出现多次
    虚基类,无论出现多少次,派生类中只会包含一个虚基类内容
    虚继承只对D类有意义,对B,C是没有意义的,只对从B,C派生出来的类
    一旦有了虚继承,子类就不用初始化父类了(B,C不用初始化A),需要有孙类初始化(D初始化A)

    class A
    {};
    
    class B:virtual public A
    {};
    
    class C:virtual public A
    {};
    
    class D:public B,C
    {};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    30、类型转换构造函数

    1、可以将一个数字转换成一个类对象。
    类构造函数没有返回值
    特点:
    只有一个参数,不是本类的const 引用,该参数其实就是待转换的数据类型
    在类型转换函数中,需要指定转换的方法

    class A
    {
    public:
    	A(int a)
    	{
    		m_x = a;
    	}
    	int m_x;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    31、类型转换运算符

    格式:
    operator type() const
    type表示要转换为的类型,能够作为函数返回类型的,都可以作为type

    class A
    {
    public:
    	operator int()const
    	{
    		return m_i;
    	}
    	int m_i;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    32、类对象转换成函数指针类型

    class A
    {
    // 定义一个函数指针类型
    typedef void(*fpfun)(int);
    //using fpfun = void(*)(int);
    public:
    	static void fun(int vtl)
    	{
    		cout<<vtl;
    	}
    	// 类对象转换成函数指针类型
    	operator fpfun()
    	{
    		return fun;
    	}
    };
    
    A a;
    a(11);	// 调用类型转换运算符,然后返回了静态函数的地址,此时a表示拿到了静态函数地址,进行调用
    
    a.operator A::fpfun()(1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    33、类成员函数指针,指向类成员函数

    class A
    {
    public:
    	void pffun(int i){}
    	virtual void virfun(int i){}
    	static void statifun(int i){}
    };
    
    typedef void(A::*mypoint)(int);	// 定义类成员函数指针
    mypoint = &A::ptfun;
    // 成员函数属于类,不属于类对象,
    // 如果要使用这个类成员函数指针,就必须要绑定到类中
    A a,*aptr;
    aptr = &a;
    (a.*mypoint)(100);
    (aptr->*mypoint)(100);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    类成员变量指针,他没有真正意义的地址,而是该类对象的偏移量

    class A
    {
    public:
    	int m_a;
    };
    
    int A::*a = &A::m_a;
    A a1;
    // 通过类成员变量指针修改成员变量
    a1.*a = 11;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    Java Field.set()方法具有什么功能呢?
    使用 ELK 收集日志
    前端CSS零碎知识点
    Linux实操篇-RPM 与 YUM
    【我的日志】关于我可爱的新同事
    《数字图像处理-OpenCV/Python》连载(26)绘制椭圆和椭圆弧
    【RabbitMQ】SpringBoot整合RabbitMQ实现延时队列
    MapStruct的一些常规用法
    go开发之个微机器人的二次开发
    Android Jetpack组件架构 :LiveData的使用和原理
  • 原文地址:https://blog.csdn.net/weixin_45715405/article/details/126376540