C++知识点

虚函数与纯虚函数

虚函数是允许被其子类重新定义的成员函数。

虚函数的声明:virtual returntype func(parameter);
引入虚函数的目的是为了动态绑定;

纯虚函数声明:virtual returntype func(parameter)=0;
引入纯虚函数是为了派生接口。(使派生类仅仅只是继承函数的接口)

基类为什么需要虚析构函数

防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。

static 和const分别怎么用,类里面static和const可以同时修饰成员函数吗?

static的作用

对变量

1.局部变量:

在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。

1)内存中的位置:静态存储区

2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)

3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。

注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。

2.全局变量

在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。

1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)

2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)

3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。

注:static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:(1)不会被其他文件所访问,修改(2)其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的.c文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。

对类中的

1.成员变量

用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。

特点:
1.不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。

2.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。

3.静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。

2.成员函数

用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不需要加static修饰符。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。

static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态

const的作用

1.限定变量为不可修改。

2.限定成员函数不可以修改任何数据成员。

3.const与指针:

const char *p 表示 指向的内容不能改变。

char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。

指针和引用的区别

本质上的区别是,指针是一个新的变量,只是这个变量存储的是另一个变量的地址,我们通过访问这个地址来修改变量。

而引用只是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行操作,因此以达到修改变量的目的。

注:

(1) 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

(2)可以有const指针,但是没有const引用(const引用可读不可改,与绑定对象是否为const无关)

注:引用可以指向常量,也可以指向变量。例如int &a=b,使引用a指向变量b。而为了让引用指向常量,必须使用常量引用,如const int &a=1; 它代表的是引用a指向一个const int型,这个int型的值不能被改变,而不是引用a的指向不能被改变,因为引用的指向本来就是不可变的,无需加const声明。即指针存在常量指针int const *p和指针常量int *const p,而引用只存在常量引用int const &a,不存在引用常量int& const a。

(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

(7)指针和引用的自增(++)运算意义不一样;

(8)指针使用时需要解引用(*),引用则不需要;

多态的用途

C++ 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。

1.定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。

2.实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。

注:多态与非多态的实质区别就是函数地址是静态绑定还是动态绑定。如果函数的调用在编译器编译期间就可以确定函数的调用地址,并产生代码,说明地址是静态绑定的;如果函数调用的地址是 需要在运行期间才确定,属于动态绑定。

3.目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。

4.用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。

用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

关于重载、重写、隐藏的区别
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
Override(覆盖或重写):是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。
Overwrite(重写):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

new和malloc的区别

  • new是运算符,malloc()是一个库函数;
  • new会调用构造函数,malloc不会;
  • new返回指定类型指针,malloc返回void*指针,需要强制类型转换;
  • new会自动计算需分配的空间,malloc不行;
  • new可以被重载,malloc不能。

C++的内存分区

  • 栈区(stack):主要存放函数参数以及局部变量,由系统自动分配释放。
  • 堆区(heap):由用户通过 malloc/new 手动申请,手动释放。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局/静态区:存放全局变量、静态变量;程序结束后由系统释放。
  • 字符串常量区:字符串常量就放在这里,程序结束后由系统释放。
  • 代码区:存放程序的二进制代码。

C++11的新特性

  • auto类型推导
  • 范围for循环
  • lambda函数
  • override 和 final 关键字
  • 空指针常量nullptr
  • 线程支持、智能指针等
/*如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。

  所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:*/class A
{
    virtual void foo();
}
class B :public A
{
    void foo(); //OK
    virtual foo(); // OK
    void foo() override; //OK
}

class A
{
    virtual void foo();
};
class B :A
{
    virtual void f00(); //OK
    virtual void f0o()override; //Error 
};
/*当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:*/

class Base
{
    virtual void foo();
};
 
class A : Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
    void bar() final; // Error: 父类中没有 bar虚函数可以被重写或final
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

const与#define的区别

1.编译器处理方式
define – 在预处理阶段进行替换
const – 在编译时确定其值

2.类型检查
define – 无类型,不进行类型安全检查,可能会产生意想不到的错误
const – 有数据类型,编译时会进行类型检查

3.内存空间
define – 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大
const – 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝

4.其他
在编译时, 编译器通常不为const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
宏替换只作替换,不做计算,不做表达式求解。

悬空指针与野指针区别

  • 悬空指针:当所指向的对象被释放或者收回,但是没有让指针指向NULL;
  • 野指针:那些未初始化的指针;

struct和class的区别

1、默认的继承权限

struct默认是公有继承(public),class默认是私有继承(private)

2、关于默认访问权限

class中默认的成员访问权限是private的,而struct中则是public的。

3、关于大括号初始化问题

struct在C语言中:
在C语言中,我们知道struct中是一种数据类型,只能定义数据成员,不能定义函数,这是因为C语言是面向过程的,面向过程认为数据和操作是分开的,所以C语言中的struct可以直接使用大括号对所有数据成员进行初始化
例如:

struct test
{
    int a;
    int b;
};
//初始化
test A={1,2};//完全可以

在C++中class和struct的区别:
在C++中对struct的功能进行了扩展,struct可以被继承,可以包含成员函数,也可以实现多态,当用大括号对其进行初始化需要注意:

当struct和class中都定义了构造函数,就不能使用大括号对其进行初始化
若没有定义构造函数,struct可以使用{ }进行初始化,而只有当class的所有数据成员及函数为public时,可以使用{ }进行初始化
所以struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

4、关于模板

在模板中,类型参数前面可以使用class或typename,如果使用struct,则含义不同,struct后面跟的是“non-type template parameter”,而class或typename后面跟的是类型参数。

virtual, inline, decltype,volatile,static, const关键字的作用?使用场景?

  • inline:在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
#include <stdio.h>//函数定义为inline即:内联函数inline char* dbtest(int a) {
    return (i % 2 > 0) ? "奇" : "偶";
} 

int main(){
   int i = 0;
   for (i=1; i < 100; i++) {
       printf("i:%d    奇偶性:%s /n", i, dbtest(i));    
   }
}//在for循环的每个dbtest(i)的地方替换成了 (i % 2 > 0) ? "奇" : "偶",避免了频繁调用函数,对栈内存的消耗
  • decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。

  • volatile:volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

深拷贝与浅拷贝的区别?

1.什么时候用到拷贝函数?
a.一个对象以值传递的方式传入函数体;
b.一个对象以值传递的方式从函数返回;
c.一个对象需要通过另外一个对象进行初始化。
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝;

2.是否应该自定义拷贝函数?
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

3.什么叫深拷贝?什么是浅拷贝?两者异同?
如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

4.深拷贝好还是浅拷贝好?
如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

C++面试题之浅拷贝和深拷贝的区别

C++拷贝构造函数(深拷贝,浅拷贝)

拷贝构造函数

对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=88;
int b=a;
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。

#include <iostream>using namespace std;

class CExample {private:
     int a;
public:
     CExample(int b)
     { a=b;}
     void Show ()
     {
        cout<<a<<endl;
    }
};

int main(){
     CExample A(100);
     CExample B=A;
     B.Show ();
     return 0;
} 

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
下面举例说明拷贝构造函数的工作过程。

#include <iostream>using namespace std;

class CExample {private:
    int a;
public:
    CExample(int b)
    { a=b;}
    
    CExample(const CExample& C)
    {
        a=C.a;
    }
    void Show ()
    {
        cout<<a<<endl;
    }
};

int main(){
    CExample A(100);
    CExample B=A;
    B.Show ();
    return 0;
} 

CExample(const CExample& C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。

C++类中数据成员初始化顺序?

1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。

2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。

3.类中const成员常量必须在构造函数初始化列表中初始化。

4.类中static成员变量,只能在类内外初始化(同一类的所有实例共享静态成员变量)。

初始化顺序:

1) 基类的静态变量或全局变量
2) 派生类的静态变量或全局变量
3) 基类的成员变量
4) 派生类的成员变量

结构体内存对齐问题?结构体/类大小的计算?

结构体大小的计算

注:内存对齐是看类型,而不是看总的字节数。比如:

#include<iostream>using namespace std;

struct AlignData1
{
    int a;
    char b[7];//a后面并不会补上3个字节,而是由于char的类型所以不用补。
    short c;
    char d;
}Node;
struct AlignData2
{
    bool a;
    int b[2];//a后面并不会补上7个字节,而是根据int的类型补3个字节。
    int c;
    int d;
}Node2;
int main(){
    cout << sizeof(Node) << endl;//16
    cout << sizeof(Node2) << endl;//20
    system("pause");
    return 0;
}

补充:

每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节。
最终占用字节数为成员类型中最大占用字节数的整数倍。
一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。

C++中结构体的大小与内存对齐

struct AlignData1
{
    char c;
    short b;
    int i;
    char d;
}Node;

这个结构体在编译以后,为了字节对齐,会被整理成这个样子:
struct AlignData1
{
    char c;
    char padding[1];
    short b;
    int i;
    char d;
    char padding[3];
}Node;

含有虚函数的类sizeof大小

联合体的大小计算

联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:
1)大小足够容纳最宽的成员;
2)大小能被其包含的所有基本数据类型的大小所整除。

union U1  
{  
    int n;  
    char s[11];  
    double d;  
};  //16,char s[11]按照char=1可以整除
  
union U2  
{  
    int n;  
    char s[5];  
    double d;  
};  //8

static_cast, dynamic_cast, const_cast, reinpreter_cast的区别?

补充:static_cast与dynamic_cast

  • cast发生的时间不同,一个是static编译时,一个是runtime运行时;
  • static_cast是相当于C的强制类型转换,用起来可能有一点危险,不提供运行时的检查来确保转换的安全性。
  • dynamic_cast用于转换指针和和引用,不能用来转换对象 ——主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息。

static_cast, dynamic_cast, const_cast探讨

智能指针

特点

  • 智能指针是在 <memory> 头文件中的std命名空间中定义的,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它们对RAII“获取资源即初始化”编程至关重要,RAII的主要原则是为将任何堆分配资源(如动态分配内存或系统对象句柄)的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。
  • 智能指针的设计思想:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
  • unique_ptr只允许基础指针的一个所有者。unique_ptr小巧高效;大小等同于一个指针且支持右值引用,从而可实现快速插入和对STL集合的检索。
  • shared_ptr采用引用计数的智能指针,主要用于要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时)的情况。当所有的shared_ptr所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。最安全的分配和使用动态内存的方法是调用make_shared标准库函数,此函数在动态分配内存中分配一个对象并初始化它,返回对象的shared_ptr。

C++智能指针简单剖析

举例

unique_ptr

struct Person{
    ...
}

unique_ptr<Person> person = new Person();//创建指针的方式1auto ptr = make_unique<Person>();//创建指针的方式2unique_ptr<Person> p2 = move(person);//person 的所有权交给p2,person为空,只能有一个对象拥有所有权

shared_ptr

struct Person{
    ...
}

shared_ptr<Person> ptr = new Person();

auto ptr1 = make_shared<Person>();
shared_ptr<Person> ptr2 = ptr1;
cout << ptr1.use_count(); // 2

计算类大小例子

class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位机器)/8(64位机器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;

类中用static声明的成员变量不计算入类的大小中,因为static data不是实例的一部分。static的属于全局的,他不会占用类的存储,他有专门的地方存储 (全局变量区)

大端与小端的概念?各自的优势是什么?

区别

  • 大端与小端是用来描述多字节数据在内存中的存放顺序,即字节序。大端(Big Endian)指低地址端存放高位字节,小端(Little Endian)是指低地址端存放低位字节。
  • 需要记住计算机是以字节为存储单位。
  • 为了方便记忆可把大端和小端称作高尾端和低尾端,eg:如果是高尾端模式一个字符串“11223344”把尾部“44”放在地址的高位,如果是地尾端模式,把“44”放在地址的低位。

各自优势
Big Endian:符号位的判定固定为第一个字节,容易判断正负。
Little Endian:长度为1,2,4字节的数,排列方式都是一样的,数据类型转换非常方便。

C++中*和&同时使用是什么意思?

template <class T>
void InsertFront(Node<T>* & head, T item)
上面一个函数的声明,其中第一个参数*和&分别是什么意思?
head是个指针,前面为什么加个&

本来“* head”代表的是传指针的,但是只能改变head指向的内容,而“* &head”意思是说head是传进来的指针的同名指针,就能既改变head指向的内容,又能改变head这个指针。比如:main()有个Node<int> p,int t;当调用insertFront(p,t)是,如果template <class T> void InsertFront(Node<T>* & head, T item)中有对head进行赋值改变时,main()中的p也会跟着改变,如果没有&这个别名标识时,p则不会随着head的改变而改变。

C++对象的copy constructor与copy assignment的区别与联系

copy constructor:

从一个已有的对象来构造另一个对象;
包括:
用已有对象来初始化新声明的对象;
将对象按值传递给函数作为参数;
函数按值返回对象。

copy assignment:

将已有的对象赋值个另一个已有的对象;
实例:
Person A(B); // copy constructor
Person C=B; //copy constructor
Function1(D); //copy constructor
B= Function2(…); //copy constructor
Person D;
D=B; //copy assignment

定义一个空类编译器做了哪些操作?

如果你只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建。所有这些函数都是inline和public的。
定义一个空类例如:

class Empty
{
}

一个空的class在C++编译器处理过后就不再为空,编译器会自动地为我们声明一些member function,一般编译过就相当于:

class Empty
{public:
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
};

友元函数和友元类

友元提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。c++中的友元为封装隐藏这堵不透明的墙开了一个小孔,外界可以通过这个小孔窥视内部的秘密。

友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

友元函数

友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:

friend 类型 函数名(形式参数);
友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
友元函数的调用与一般函数的调用方式和原理一致。

友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;
其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。

#include <iostream>using namespace std;

class Radius
{
    friend class Circle;           //声明Circle为Radius的友元类
    friend void Show_r(Radius &n); //声明Show_r为友元函数public:
    Radius(int x)
    {
        r = x;
    }
    ~Radius()
    {
    }

private:
    int r;
};

void Show_r(Radius &n){
    cout << "圆的半径为: " << n.r << endl; //调用Radius对象的私有成员变量r
}

class Circle
{public:
    Circle() {}
    ~Circle() {}
    double area(Radius a)
    {
        s = a.r * a.r * 3.1415926; //调用Radius对象的私有成员变量r
        return s;
    }

private:
    double s;
};

int main(int argc, char *argv[]){
    Radius objRadius(9);
    Circle objCircle;

    Show_r(objRadius);
    cout << "面积为:" << objCircle.area(objRadius) << endl;

    return 0;
}

被声明两个类的友元声明

class Window; // 只声明

class Screen
{
    friend bool is_equal(Screen &, Window &);
    // ...
};

class Window
{
    friend bool is_equal(Screen &, Window &);
    // ...
};

使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

什么情况下,类的析构函数应该声明为虚函数?为什么?

基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

哪些函数不能成为虚函数?

不能被继承的函数和不能被重写的函数。

1)普通函数

普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。

而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。

2)友元函数

友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

3)构造函数

首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。

4)内联成员函数

我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。

5)静态成员函数

首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。

编写一个有构造函数,析构函数,赋值函数,和拷贝构造函数的String类

头文件如下:

//.hclass String
{public:
    String(const char *str);
    String(const String &other);
    ~String();
    String &operate = (const String &other);

private:
    char *m_data;
};

C++如下:

//.cpp
String::String(const char *str)
{
    if (str == NULL)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        int length = strlen(str);
        m_data = new char[length + 1];
        strcpy(m_data, str);
    }
}

String::String(const String &other)
{
    int length = strlen(other.m_data);
    m_data = new char[length + 1];
    strcpy(m_data, other.m_data);
}

String::~String()
{
    delete[] m_data;
}

String::String &operate = (const String &other)
{
    if (&other == *this)
        return *this; //检查自赋值
    delete[] m_data;  //释放原有的内存资源
    int length = strlen(other.m_data);
    m_data = new char[length + 1];
    strcpy(m_data, other.m_data);
    return *this; //返回本对象的引用
}

C++ this指针的理解

this指针指向当前的对象

  1. this指针的用处:
    一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
    this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。例如,调用date.SetMonth(9) <===> SetMonth(&date, 9),this帮助完成了这一转换 .
    在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无需通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看成this的隐式使用。
    this的目的总是指向这个对象,所以this是一个常量指针,我们不允许改变this中保存的地址
  2. this指针的使用:
    一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;另外一种情况是当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
    3.this指针是存在与类的成员函数中,指向被调用函数所在的类实例的地址。

C++类中this指针的理解

智能指针

智能指针的作用

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

理解智能指针需要从下面三个层次:

  • 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  • 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
  • 智能指针还有一个作用是把值语义转换成引用语义。

智能指针的使用

shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get函数获取原始指针
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。
#include <iostream>#include <memory>

int main(){
    {
        int a = 10;
        std::shared_ptr<int> ptra = std::make_shared<int>(a);
        std::shared_ptr<int> ptra2(ptra);           //copy
        std::cout << ptra.use_count() << std::endl; //2

        int b = 20;
        int *pb = &a;
        //std::shared_ptr<int> ptrb = pb;  //error
        std::shared_ptr<int> ptrb = std::make_shared<int>(b);
        ptra2 = ptrb;    //assign
        pb = ptrb.get(); //获取原始指针

        std::cout << ptra.use_count() << std::endl; //1
        std::cout << ptrb.use_count() << std::endl; //2
    }
}

智能指针shared_ptr的用法

unique_ptr

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

#include <iostream>#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
        std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
        uptr2.release(); //释放所有权
    }
    //超過uptr的作用域,內存釋放
}

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

#include <iostream>#include <memory>

int main() {
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        std::cout << sh_ptr.use_count() << std::endl;

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;

        if(!wp.expired()){
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;
        }
    }
    //delete memory
}

循环引用问题

示例1
在Man类内部会引用一个Woman,Woman类内部也引用一个Man。当一个man和一个woman是夫妻的时候,他们直接就存在了相互引用问题。man内部有个用于管理wife生命期的shared_ptr变量,也就是说wife必定是在husband去世之后才能去世。同样的,woman内部也有一个管理husband生命期的shared_ptr变量,也就是说husband必须在wife去世之后才能去世。这就是循环引用存在的问题:husband的生命期由wife的生命期决定,wife的生命期由husband的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。
     解决std::shared_ptr循环引用问题的钥匙在weak_ptr手上。weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放。另外很自然地一个问题是:既然weak_ptr不增加资源的引用计数,那么在使用weak_ptr对象的时候,资源被突然释放了怎么办呢?呵呵,答案是你根本不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()方法。

细节:shared_ptr实现了operator bool() const方法来判断一个管理的资源是否被释放。

#include <iostream>#include <memory>

class Woman;class Man
{private:
    std::weak_ptr<Woman> _wife;
    //std::shared_ptr<Woman> _wife;public:
    void setWife(std::shared_ptr<Woman> woman)
    {
        _wife = woman;
    }

    void doSomthing()
    {
        if (_wife.lock())
        {
        }
    }

    ~Man()
    {
        std::cout << "kill man\n";
    }
};

class Woman
{private:
    //std::weak_ptr<Man> _husband;
    std::shared_ptr<Man> _husband;

public:
    void setHusband(std::shared_ptr<Man> man)
    {
        _husband = man;
    }
    ~Woman()
    {
        std::cout << "kill woman\n";
    }
};

int main(int argc, char **argv){
    std::shared_ptr<Man> m(new Man());
    std::shared_ptr<Woman> w(new Woman());
    if (m && w)
    {
        m->setWife(w);
        w->setHusband(m);
    }
    return 0;
}

示例2

#include <iostream>#include <memory>

class Child;class Parent;

class Parent
{private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;

public:
    void setChild(std::shared_ptr<Child> child)
    {
        this->ChildPtr = child;
    }

    void doSomething()
    {
        //new shared_ptr
        if (this->ChildPtr.lock())
        {
        }
    }

    ~Parent()
    {
    }
};

class Child
{private:
    std::shared_ptr<Parent> ParentPtr;

public:
    void setPartent(std::shared_ptr<Parent> parent)
    {
        this->ParentPtr = parent;
    }
    void doSomething()
    {
        if (this->ParentPtr.use_count())
        {
        }
    }
    ~Child()
    {
    }
};

int main(){
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::cout << p.use_count() << std::endl; // 1

        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        std::cout << p.use_count() << std::endl; // 1

        c->setPartent(p);
        std::cout << p.use_count() << std::endl; // 2

        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl; // 0
    std::cout << wpc.use_count() << std::endl; // 0
    return 0;
}

内联函数和宏定义的区别

  1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。

    内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。

  2. 宏定义是在预处理的时候把所有的宏名用宏体来替换,简单的说就是字符串替换
    内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率

  3. 宏定义是没有类型检查的,无论对还是错都是直接替换

    内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

  4. 宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率

内联函数与普通函数的区别

  1. 内联函数和普通函数的参数传递机制相同,但是编译器会在每处调用内联函数的地方将内联函数内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷。

  2. 普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。

    内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有N次调用了内联函数则会有N次展开函数代码。

  3. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句。如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行。

C++中成员函数能够同时用static和const进行修饰?

不能。

C++编译器在实现const的成员函数(const加在函数右边)的时候为了确保该函数不能修改类的中参数的值,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

即:static修饰的函数表示该函数是属于类的,而不是属于某一个对象的,没有this指针。const修饰的函数表示该函数不能改变this中的内容,会有一个隐含的const this指针。两者是矛盾的。

构造函数的初始化列表

构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。

class CExample {public:
    int a;
    float b;
    //构造函数初始化列表
    CExample(): a(0),b(8.8)
    {}
    //构造函数内部赋值
    CExample()
    {
        a=0;
        b=8.8;
    }
};

上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。

explicit关键字

explicit关键字只能用于类内部的构造函数声明上,而不能用在类外部的函数定义(函数实现)上,它的作用是防止类构造函数的隐式自动转换;explicit关键字作用于单个参数的构造函数,如果构造函数有多个参数,但是从第二个参数开始,如果各参数均有默认赋值,也可以应用explicit关键字。

当构造函数只有一个参数时,会进行自动隐式转换,当构造函数参数个数超过或等于两个时自动取消隐式转换,当只有一个必须输入的参数,其余的为有默认值的参数时使用explicit也起作用。

一般只将有单个参数的构造函数声明为explicit,而拷贝构造函数不要声明为explicit。

explicit关键字只能对用户自己定义的对象起作用,不对默认构造函数起作用。此关键只能够修饰构造函数。无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。

decltype 关键字

有时我们希望从表达式的类型推断出要定义的变量类型,但是不想用该表达式的值初始化变量(初始化可以用auto)。

decltype的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

decltype用法

基本用法

int getSize();
​
int main(void){
    int tempA = 2;
    
    /*1.dclTempA为int.*/
    decltype(tempA) dclTempA;
    /*2.dclTempB为int,对于getSize根本没有定义,但是程序依旧正常,因为decltype只做分析,并不调用getSize().*/
    decltype(getSize()) dclTempB;
​
    return 0;
}

与const结合

    double tempA = 3.0;
    const double ctempA = 5.0;
    const double ctempB = 6.0;
    const double *const cptrTempA = &ctempA;
    
    /*1.dclTempA推断为const double(保留顶层const,此处与auto不同)*/
    decltype(ctempA) dclTempA = 4.1;
    /*2.dclTempA为const double,不能对其赋值,编译不过*/
    dclTempA = 5;
    /*3.dclTempB推断为const double * const*/
    decltype(cptrTempA) dclTempB = &ctempA;
    /*4.输出为4(32位计算机)和5*/
    cout<<sizeof(dclTempB)<<"    "<<*dclTempB<<endl;
    /*5.保留顶层const,不能修改指针指向的对象,编译不过*/
    dclTempB = &ctempB;
    /*6.保留底层const,不能修改指针指向的对象的值,编译不过*/
    *dclTempB = 7.0;

与引用结合

    int tempA = 0, &refTempA = tempA;
​
    /*1.dclTempA为引用,绑定到tempA*/
    decltype(refTempA) dclTempA = tempA;
    /*2.dclTempB为引用,必须绑定到变量,编译不过*/
    decltype(refTempA) dclTempB = 0;
    /*3.dclTempC为引用,必须初始化,编译不过*/
    decltype(refTempA) dclTempC;
    /*4.双层括号表示引用,dclTempD为引用,绑定到tempA*/
    decltype((tempA)) dclTempD = tempA;
    
    const int ctempA = 1, &crefTempA = ctempA;
    
    /*5.dclTempE为常量引用,可以绑定到普通变量tempA*/
    decltype(crefTempA) dclTempE = tempA;
    /*6.dclTempF为常量引用,可以绑定到常量ctempA*/
    decltype(crefTempA) dclTempF = ctempA;
    /*7.dclTempG为常量引用,绑定到一个临时变量*/
    decltype(crefTempA) dclTempG = 0;
    /*8.dclTempH为常量引用,必须初始化,编译不过*/
    decltype(crefTempA) dclTempH;
    /*9.双层括号表示引用,dclTempI为常量引用,可以绑定到普通变量tempA*/
    decltype((ctempA))  dclTempI = ctempA;

与指针结合

    int tempA = 2;
    int *ptrTempA = &tempA;
    /*1.常规使用dclTempA为一个int *的指针*/
    decltype(ptrTempA) dclTempA;
    /*2.需要特别注意,表达式内容为解引用操作,dclTempB为一个引用,引用必须初始化,故编译不过*/
    decltype(*ptrTempA) dclTempB;

decltype总结

decltype和auto都可以用来推断类型,但是二者有几处明显的差异:

  1. auto忽略顶层const,decltype保留顶层const;
  2. 对引用操作,auto推断出原有类型,decltype推断出引用;
  3. 对解引用操作,auto推断出原有类型,decltype推断出引用;
  4. auto推断时会实际执行,decltype不会执行,只做分析。总之在使用中过程中和const、引用和指针结合时需要特别小心。

emplace_back

c++开发中我们会经常用到插入操作对stl的各种容器进行操作,比如vector,map,set等。在引入右值引用,转移构造函数,转移复制运算符之前,通常使用push_back()向容器中加入一个右值元素(临时对象)时,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费。
引入了右值引用,转移构造函数后,push_back()右值时就会调用构造函数和转移构造函数,如果可以在插入的时候直接构造,就只需要构造一次即可。这就是c++11 新加的emplace_back。
例如:

#include <vector>#include <string>#include <iostream>using namespace std;
struct Youbain
{
 
        string _contry;
        string _privence;
        int _number;
                
        Youbain(string&& contry, string&& privence, int number)
                :_contry(std::move(contry)), _privence(std::move(privence)), _number(number)
        {
                cout << "转移构造" << endl;
        }
        Youbain(Youbain&& y)
                :_contry(std::move(y._contry)), _privence(std::move(y._privence)), _number(y._number)
        {
                cout << "转移拷贝" << endl;
        }
        Youbain& operator = (const Youbain& y) = default;
}
 
int main(){
        vector<Youbain> el;
        el.emplace_back("China", "Shannxi", 610000);
        el.push_back(Youbain("China", "Beijing", 10000));
  • 在执行emplace_back的时候,只调用了转移构造函数,在插入的时候直接构造,效率更高,减少额外空间的开辟
    运行结果:转移构造
  • 在执行push_back的时候,调用了构造和拷贝构造函数,因为在使用push_back()向容器中加入一个右值元素(临时对象)时,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费。
    运行结果:转移构造
    转移拷贝

constexpr

在C++11中,我们可以通过声明constexpr让编译器确定一个变量是不是常量表达式,声明为constexpr的变量是const类型的变量,它必须由常量表达式来初始化。

constexpr int mf = 20; // 常量表达式constexpr int limit = mf + 1; // 常量表达式constexpr int sz = size(); // 如果size()是常量表达式则编译通过,否则报错

在C++11中,我们可以声明某些函数是常量表达式,这样的函数必须在编译期间计算出它们的值,这样的函数必须满足以下条件:

  • 返回值和参数必须是Literal类型
  • 函数体必须只包含一个return语句
  • 函数提可以包含其他的语句,但是这些语句不能在运行期起作用
  • 函数可以不返回常量,但是在调用的时候实参必须传入常量表达式

std::atomic

原子类型是封装了一个值的类型,它的访问保证不会导致数据的竞争,并且可以用于在不同的线程之间同步内存访问。

c++11 多线程(3)atomic 总结

右值引用

C++11有哪些新特性

1)关键字及新语法:auto、nullptr、for
2)STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
3)多线程:std::thread、std::atomic、std::condition_variable
4)智能指针内存管理:std::shared_ptr、std::weak_ptr
5)其他:std::function、std::bind和lamda表达式

template

// function template II#include <iostream>using namespace std;

template <class T>
T GetMax (T a, T b) {
      return (a>b?a:b);
}

int main () {
    int i=5, j=6, k;
    long l=10, m=5, n;
    k=GetMax(i,j);
    n=GetMax(l,m);
    cout << k << endl;
    cout << n << endl;
    return 0;
}

const 的若干种用法

C++中const关键字的使用方法

const限定符和static的区别

  1. const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。
  2. static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。
  3. 在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate=2.25; static关键字只能用于类定义体内部的声明中,定义时不能标示为static
  4. 在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
  5. const数据成员,只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。
  6. const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static const。
    const成员函数主要目的是防止成员函数修改对象的内容。即const成员函数不能修改成员变量的值,但可以访问成员变量。当方法成员函数时,该函数只能是const成员函数。
  7. static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1、不能直接存取类的非静态成员变量,调用非静态成员函数

mutable

在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

无锁环形队列的实现

1. 环形队列的存储结构
链表和线性表都是可以的,但几乎都用线性表实现,比链表快很多,原因也是显而易见的,因为访问链表需要挨个遍历。
2. 读写index
有2个index很重要,一个是写入index,标示了当前可以写入元素的index,入队时使用。一个是读取index,标示了当前可以读取元素的index,出队时使用。
3. 元素状态切换
有种很巧妙的方法,就是在队列中每个元素的头部加一个元素标示字段,标示这个元素是可读还是可写,而这个的关键就在于何时设置元素的可读可写状态,参照linux内核实现原理,当这个元素读取完之后,要设置可写状态,当这个元素写入完成之后,要设置可读状态。

环形无锁队列

参考链接:https://www.cnblogs.com/inception6-lxc/p/8686156.htm

posted @ 2020-06-11 14:34  晨哥小记  阅读(307)  评论(0编辑  收藏  举报