基础知识_Cpp

文章目录
  1. 1. 基础语法
    1. 1.1. static关键字
    2. 1.2. const关键字
    3. 1.3. friend关键字
    4. 1.4. mutable关键字
    5. 1.5. assert关键字
    6. 1.6. using namespace std
    7. 1.7. noncopyable禁止拷贝
  2. 2. C++&面向对象
    1. 2.1. RAII机制
    2. 2.2. 什么是虚函数,实现原理是什么?
    3. 2.3. 面向对象三大特性
    4. 2.4. 编译时多态是怎样的
    5. 2.5. 类成员的权限控制
    6. 2.6. struct和class的区别
    7. 2.7. Cpp中如何禁止一个类创建对象
    8. 2.8. 如何限制类只能在堆或栈上创建对象
    9. 2.9. 带默认参数的构造函数
    10. 2.10. Cpp构造函数私有化
    11. 2.11. 拷贝构造函数的调用时机
    12. 2.12. 在一个有指针对象的类中至少要实现哪三个函数
    13. 2.13. 如果没有实现拷贝赋值运算符可能会遇到什么问题(深拷贝、浅拷贝)
    14. 2.14. 指针和引用的区别
    15. 2.15. volatile
    16. 2.16. 结构体内存对齐
    17. 2.17. new和malloc的区别
    18. 2.18. ptrdiff_t
  3. 3. STL
    1. 3.1. 讲一下类型萃取机制
    2. 3.2. STL中分别有哪些容器,底层实现是什么
    3. 3.3. sort函数怎么实现的
    4. 3.4. 什么是仿函数
    5. 3.5. 哪些情况迭代器会失效
    6. 3.6. vector使用时注意问题
    7. 3.7. []与at()区别
    8. 3.8. vector扩容原理
    9. 3.9. deque扩容原理
  4. 4. C++11
    1. 4.1. std::move()语义原理
    2. 4.2. std::forward()
    3. 4.3. 四种智能指针
      1. 4.3.1. shared_ptr
      2. 4.3.2. weak_ptr
      3. 4.3.3. unique_ptr
      4. 4.3.4. auto_ptr(已遗弃)
    4. 4.4. 实现一个shared_ptr智能指针
    5. 4.5. shared_ptr的线程安全性
    6. 4.6. C++11的四种强制类型转换
    7. 4.7. 列表初始化
    8. 4.8. decltype作用以及与auto区别。

Cpp基础知识与常见问题。

基础语法

static关键字

  • 修饰全局变量,在堆区分配内存;默认初始化为零;限定作用域为当前文件。
  • 修饰局部变量时,在堆区分配内存;只有首次定义时被初始化,直到程序运行结束才释放;作用域为局部作用域。

  • 修饰普通函数,不能修改任何非static对象;该函数的作用域为当前文件 。

  • 修饰类内成员,堆区分配内存;程序运行时就被初始化,直到程序结束;成员归属于类,被所有对象共享;可以通过”类名::静态成员”或”对象.静态成员”访问
  • 修饰类内函数,只能访问类内静态成员或调用类内静态函数,但是普通函数可以访问静态成员和静态函数;可以通过类名调用或对象调用。
  • C++中的static关键字的总结

const关键字

  • 特性:(1)被修饰的对象不是常量,是一个只读变量(不能放在case关键字后面也说明const不是一个常量);(2)定义时赋值,之后不允许修改。
  • 修饰普通变量
1
2
3
const int a;
a还是一个int型变量,const int 顺序可以互换:
int const a;
  • 修饰数组
1
2
3
const int a[5];
int const a[5];
只读数组
  • 修饰指针
1
2
3
4
const char *p;
char const *p;//这两种,const都是修饰*p,则p指向的变量只读。(与下一种对照记忆)
char * const p;//const很明显修饰指针p,则指针p不能被修改。
const char * const p;//指针p不能被修改,指向的对象也不能被修改。
  • 修饰函数形参
1
2
3
4
5
6
7
8
9
void apple(const char *a){//防止修改a指向的字符串
...
}
void apple(char * const a){//防止修改a本身
...
}
void apple(const vector<int> &num){//防止修改容器内容
...
}
  • 修饰函数
1
2
3
int getSum() const {//修饰函数,表示该函数内不能修改变量
return get_Name_sum;
}

friend关键字

friend提供了在类外访问类的私有成员的能力,friend可以修饰函数或类。当在类内声明一个友元函数时,该函数可以访问类的私有成员。当在类内声明友元类时,则友元类可以访问当前类的私有成员。

mutable关键字

如果需要在const成员方法中修改一个成员变量的值,那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制。

assert关键字

assert是一个宏,用于在DEBUG版本下判断表达式的真假,如果表达式为假,它会先向stderr打印错误信息,然后调用abort终止程序运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <bits/stdc++.h>
using namespace std;

int main(){
cout<<"test assert:"<<endl;
assert(2*2==4);
assert(2*2==5);

return 0;
}
/*
test assert:
20200930_test: 20200930_test.cpp:10: int main(): Assertion 2*2==5 failed.
已放弃 (核心已转储)

using namespace std

1.在头文件中一定不要使用,否则在别人引用你的头文件后,如果std中的函数名和其他库中的冲突了,可能会带来麻烦。
2.在cpp文件中:

* 在"一般情况下"可以使用,但是注意一定要在所有include语句之后使用。
* 可以在函数中使用,使其只在有限作用域内有效。
* 不直接使用命名空间using namespace xxx,可以使用using std::cin;using std::cout;这种方式。
* 也可以在需要的地方全部加上std:: 。

noncopyable禁止拷贝

1.通用的做法是写一个类noncopyable,凡是继承该类的任何类都无法复制和赋值。

  • 将拷贝构造函数和拷贝赋值运算符设置为私有,这样继承nocopyable的类给对象赋值或拷贝构造时,会先调用父类nocopyable的函数,但是这两个函数是私有的,所以会引发编译错误。
  • 将noncopyable的构造函数和析构函数设置protected,这样该类无法创建对象,但是子类中可以调用。
    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
    #include <bits/stdc++.h>
    using namespace std;

    class noncopyable {
    protected:
    noncopyable() = default;
    ~noncopyable() = default;
    private:
    noncopyable(const noncopyable&) = delete;
    const noncopyable& operator=( const noncopyable& ) = delete;
    };

    class StockFactory:noncopyable{
    public:
    StockFactory(double _price):price(_price){}
    private:
    double price;
    };

    int main(){
    StockFactory s1(10);
    //StockFactory s2=s1;

    return 0;
    }

2.使用c++11标准的简单实现:

1
2
3
4
5
6
7
class noncopyalbe{
protected:
noncopyable()=default;
~noncopyable()=default;
noncopyable(const noncopyable &)=delete;
noncopyable &operator=(const noncopyable &)=delete;
}

3.google开源项目风格指南建议的做法是使用 DISALLOW_COPY_AND_ASSIGN 宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 禁止使用拷贝构造函数和 operator= 赋值操作的宏
// 应该类的 private: 中使用

#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
在 class foo 中使用方式如下:
class Foo {
public:
Foo(int f);
~Foo();

private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};

绝大多数情况下都应使用 DISALLOW_COPY_AND_ASSIGN 宏。如果类确实需要可拷贝,应在该类的头文件中说明原由,并合理的定义拷贝构造函数和赋值操作。注意在 operator= 中检测自我赋值的情况。

C++&面向对象

RAII机制

  • RAII(Resource Acquisition Is Initialization),翻译过来是资源获取即初始化。也就是说当创建一个对象的时候,就对其进行初始化,同样当不需要该对象时,也要对其资源进行释放。比如当调用new来申请空间时、调用open()打开文件时,都需要对应的delete、close来释放资源,但是往往就忘掉释放资源。所以可以利用类的构造函数和析构函数,将需要分配资源的对象进行一层封装,将其获取资源和释放资源分别绑定到构造函数和析构函数里,这样当该对象生命周期结束,就会自己释放资源。
  • 示例一:编程中可能出现以下情况,但是中间如果发生异常或者return了,就执行不了unlock()了。就会导致该资源一直被占用了。
    1
    2
    3
    4
    5
    6
    7
    8
    std::mutex mutex_;
    void function()
    {
    mutex_.lock();
    ......
    ......
    mutex_.unlock();
    }

所以正确的方式是使用std::unique_lock或者std::lock_guard对互斥量进行状态管理:

1
2
3
4
5
6
7
std::mutex mutex_;
void function()
{
std::lock_guard<std::mutex> lock(mutex_);
......
......
}

这样只管创建一个lock对象就可以,lock生命周期结束时会自动对mutex_解锁。

什么是虚函数,实现原理是什么?

虚函数是实现运行时多态的一种机制,比如两个父类指针分别指向子类A和子类B的实例,父类指针调用虚函数时,会根据不同的子类来调用不同的函数。
当类中声明虚函数之后,编译器会在类的开始位置设置一个指针,来指向一个虚函数列表,当子类继承父类时,会一块继承这个指针,如果子类对父类中的虚函数进行了重写,就会用新函数的地址覆盖虚函数表中的旧函数。
http://taowusheng.cn/2019/05/18/20190518%20C++%E8%99%9A%E5%87%BD%E6%95%B0%E7%9B%B8%E5%85%B3%E7%9F%A5%E8%AF%86%E7%82%B9/

面向对象三大特性

  • 封装。通过设置资源的权限,来实现信息隐藏,提高安全性。一般讲数据设置私有,只提供公开接口来访问资源。
  • 继承。对事物进行抽象,将通用的特征放到基类,根据不同事物的分化,实现不同的子类。
  • 多态。分为编译时多态和运行时多态。编译时多态通过模板和函数重载实现,运行时多态通过虚函数实现。

编译时多态是怎样的

  • 第一种通过模板实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Dog{
    public:
    void sound(){
    cout<<"汪汪"<<endl;
    }
    };

    class Cat{
    public:
    void sound(){
    cout<<"喵喵"<<endl;
    }
    };

    template<typename T>
    void animalSound(T t){
    t.sound();
    }
    //===================
    Dog d;
    Cat c;
    animalSound(d);
    animalSound(c);
  • 第二种重载。函数名相同,参数不同。

类成员的权限控制

 访问权限   类内   子类   类外 
public
protect
private

struct和class的区别

  • struct默认的访问权限和继承权限是public,class默认的访问权限和继承权限是private。

Cpp中如何禁止一个类创建对象

1.将构造函数设置为protected或private。
2.在类内声明纯虚函数。

如何限制类只能在堆或栈上创建对象

1.编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就无法建立在栈上了。
缺点:(1).无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。(2).类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。(3)为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个自定义函数来构造,使用一个自定义函数来析构。

1
2
3
4
5
6
7
8
9
10
11
12
class A{
protected:
A(){}
~A(){}
public:
static A* create(){
return new A();
}
void destory(){
delete this;
}
};

2.只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。虽然你不能影响new operator的能力(因为那是C++语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将operator new()设为私有即可禁止对象被new在堆上。

1
2
3
4
5
6
7
8
class A{  
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};

带默认参数的构造函数

  • 如果不写构造函数,会有一个默认的无参构造。如果写了带参构造,编译器就不会创建无参构造了。
  • 创建一个如下带默认参数的构造函数,相当于手动创建四个构造函数的效果。
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
#include <iostream>
using namespace std;

class A{
public:
A(int a=9,int b=8,int c=7){
x=a;
y=b;
z=c;
}
void show(){
cout<<x<<" "<<y<<" "<<z<<endl;
}
private:
int x,y,z;
};

int main(){
A a1;
A a2(1);
A a3(1,1);
A a4(1,1,1);

a1.show();//9 8 7
a2.show();//1 8 7
a3.show();//1 1 7
a4.show();//1 1 1
return 0;
}

Cpp构造函数私有化

一般构造函数都是公有地,创建一个对象时就会自动调用构造函数。(对象是算作类外的,它不是类本身)
构造函数设置为私有,那岂不是没法创建对象了。但是对于强大的Cpp来说,有方法可以绕过去。
构造函数还是要调用的,我们可以在有权限的地方调用,比如static函数、友元函数或友元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

using namespace std;

class Student{
public:
static Student* makeObj(){
cout<<"hello"<<endl;
return (new Student);
}
private:
Student(){}
};

int main(){
Student* s=Student::makeObj();
return 0;
}

这种方式可以用在单例模式的实现上。

拷贝构造函数的调用时机

  • 用一个类的对象去初始化另一个对象时。
  • 往函数中传递对象参数时。
  • 从函数中返回一个对象时。

在一个有指针对象的类中至少要实现哪三个函数

  • 拷贝构造函数、拷贝赋值运算符、析构函数

如果没有实现拷贝赋值运算符可能会遇到什么问题(深拷贝、浅拷贝)

  • 浅拷贝,只拷贝指针的值,深拷贝会再开辟一块新空间,连同指针在堆中指向的内容一块拷贝过去。
  • 当一个类中含有对象指针时,如果把该类的一个对象复制给另一个对象,这时会导致两个对象中的指针指向同一块内存,此时一个对象销毁,可能会导致另一个对象中的指针指向的内容被销毁。

指针和引用的区别

指针也是一个变量,里面存储的内容是一个地址。而引用本质上是一个常量指针,引用只允许初始化,不能再修改。
编译指针和引用的代码,在汇编上是一样的:c++中,引用和指针的区别是什么? - RainMan的回答 - 知乎
https://www.zhihu.com/question/37608201/answer/545635054

volatile

  • 1.保证可见性。volatile的语义是让编译器不要对变量的值做任何假设和推理,禁止用寄存器缓存变量,每次都重新读写内存。

  • https://zhuanlan.zhihu.com/p/33074506

结构体内存对齐

对齐规则

  • 为了提高内存的读取效率,编译器使用内存对齐的技术。
  • 1.结构体内成员对齐规则:第一个成员偏移为0,其他每个成员的开始地址需要是min(当前成员大小,默认对齐字节)的整数倍。
  • 2.结构体的对齐规则:偏移地址需要是min(“默认对齐字节”,结构体内最宽成员)的整数倍。
  • 3.结构体总大小:内部最宽基本类型的整数倍。
  • 使用预编译命令可以控制默认对齐字节,#pragma pack(4)

内存对齐作用

  • 平台移植:有的硬件平台不能访问任意地址。
  • 性能原因:cpu是按块读取内存的,能够提高访问速度。

new和malloc的区别

malloc

  • malloc是一个库函数,作用是分配指定大小的空间。
  • 参数是要分配的字节数,返回void*类型的指针,返回值一般需要强制类型转换才能使用。
  • 如果申请内存失败会返回NULL。
  • 可以用realloc扩容,使用free释放内存。
  • 申请数组时:int ptr=(int)malloc(sizeof(int)*n);释放数组free(ptr);

operator new与new operator

  • operator new是一个类似加减乘除的表达式,内部会调用malloc分配内存。
  • 当A a= new A();创建新对象时,是使用的new operator。会做两件事,一是调用operator分配内存,二是调用对象的构造函数。
  • 后者编译时行为,前者运行时行为

operator new

  • 接受数据尺寸类型,返回该类型的指针。
  • 申请内存失败会抛出异常,或者执行给设定的处理函数。
  • 使用delete释放内存。

placment new

  • placement new,可以给new在指定内存区创建对象,char p[1024];int *ptr=new(p) int;
  • 注意使用”放置new”创建的对象不要使用delete,需要自己手动调用析构函数。

nothrow new

  • new函数在分配内存失败时会抛出异常,可以通过nothrow new在申请内存失败时,将返回值设置为NULL。
  • 使用方式:char myarray = new (std::nothrow)char[2047 1024 * 1024]; if(myarray==nullptr){…}

set_new_handler

  • 该函数接收一个函数指针,当new或new[]失败时,就会执行传进来的函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // new_handler example
    #include <iostream> // std::cout
    #include <cstdlib> // std::exit
    #include <new> // std::set_new_handler

    void no_memory () {
    std::cout << "Failed to allocate memory!\n";
    std::exit (1);
    }

    int main () {
    std::set_new_handler(no_memory);
    std::cout << "Attempting to allocate 1 GiB...";
    char* p = new char [1024*1024*1024];
    std::cout << "Ok\n";
    delete[] p;
    return 0;

ptrdiff_t

ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。ptrdiff_t定义在stddef.h(cstddef)这个文件内。ptrdiff_t通常被定义为long int类型。


STL

讲一下类型萃取机制

  • 为什么?当我们利用模板的参数推导机制,实现一个对不同迭代器通用的函数时,函数的参数类型(智能指针)能够推导出来,但是如果函数内部需要用到指针指向的类型,就很不方便了。再就是函数的返回值也要用到指针指向的类型时,仅利用模板的参数推导是做不到的。
  • 如何实现?首先需要每个迭代器来配合,迭代器内部应当储存所指向数据的类型value_type,然后我们利用typedef来将不同迭代器中的value_type都加个新名字。然后在需要用到萃取类型的地方,用我们的typedef所创造的新名字就行了。关键实现如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<typename I>
    struct my_traits{
    typedef typename I::value_type value_type;
    }

    //使用的时候如下:
    template<typename Iter>
    typename my_traits<Iter>::value_type
    addone(Iter iter){
    typename my_traits<Iter>::value_type tmp=*iter;
    tmp++;
    return tmp;
    }

每个类型I的迭代器都会储存指向元素的类型value_type,像原生指针、const指针不是一个类,没有value_type,就得自己实现特化版本。

STL中分别有哪些容器,底层实现是什么

  • 序列式容器:vector(连续空间)、list(双向链表)、deque(分段连续空间)。
  • 关联式容器:map、set、multimap、multiset。(都是红黑树)
  • 配置器:queue(对deque封装,可改为list)、stack(对deque封装,可改为list)、priority_queue(堆)。
  • 无序关联式容器:unordered_map、unordered_set、unordered_mulitimap、unordered_mulitiset。(都是hash表)

sort函数怎么实现的

  • sort的主体是先借助”内省式排序(__introsort_loop())”将序列排成大体有序的,然后利用插入排序再排成完全有序的。
  • 第一步的__introsort_loop()是不完全的快排和堆排序的结合体。不完全快排是指在快排递归过程中,判断当前序列的元素个数小于等于16,就退出,不再排序了,这样的结果会让不同的序列块之间是有序的。堆排序是指在当递归深度达到logn时(即快排有递归恶化的倾向出现),调用堆排序对序列进行排序。
  • 第二步的插入排序也不是标准的插入排序,也是将序列分段进行插入排序,节省了一次排序过程中的比较操作。
  • sort的实现中有很多技巧对排序进行了优化,全是为了提高效率,其最坏情况的时间复杂度也是nlogn。包括使用while循环减少一半快排的函数递归调用、插入排序分段、使用堆排序优化递归层数等。
  • 推荐阅读《STL源码剖析》 & 知无涯之std::sort源码剖析
  • 另sort为什么不直接用稳定的堆排序实现?堆排序在排序过程中是跳跃式地访问元素,缓存命中率较低。而快排每次对局部数据操作,具有较好的缓存命中率。

什么是仿函数

  • 仿函数是对一个类的括号运算符进行重载,然后可以通过函数调用的方式来调用该类所重载的运算符。

哪些情况迭代器会失效

  • 一般发生在对容器进行insert()、erase()后。
  • 当对vector插入或删除中间一个元素后,原位置之后的迭代器会失效。
  • 对list、map、set的结点进行修改后,一般只会导致当前迭代器失效。

vector使用时注意问题

*当插入或删除中间一个元素后,原位置之后的迭代器会失效。

[]与at()区别

  • []没有下标越界检查,效率更高,访问越界可能会segment fault。
  • at函数有越界检查,如果越界会抛出out_of_range异常。

vector扩容原理

在push_back()的时候会检查是否还有剩余空间,如果没有了,就申请一块原来尺寸2倍的空间,将原来的数据直接复制过去,然后把最后一个元素添加到最后面。并释放原来的空间。

deque扩容原理

deque结构:有一个map指针数组,每一个元素都指向一个缓冲区,扩容时申请空间为原map数组长度二倍,然后把原数组内容复制到新空间的中间。


C++11

std::move()语义原理

简介:

  • 理解move要先知道左值和右值,以string str=”hello”为例,str这个变量是一个左值,可以被改变。”hello”是一个右值,不能被改变了。
    然后对左值使用&进行左值引用,对右值使用&&进行右值引用。
  • 对于左值,我们可以使用&进行引用,对于右值,我们可以用&&给它续命。

    1
    2
    3
    int a=10;
    int &b=a;
    int &&c=10;
  • 如果我们就想用左值引用绑定到左值上,那就需要用到move()了。

    1
    2
    3
    4
    5
    int a=10;
    int &&b=std::move(a);
    //std::move()做的是转移控制权,将a储存的右值的所有权交给b。
    //因为转移了所有权,所以除了对a赋值或销毁外,不要再使用a。
    //注意使用该函数时加std::,避免潜在的名字冲突。
  • 从实现上讲,std::move基本等同于一个类型转换:static_cast(lvalue);

参考:

std::forward()

  • forward会保留参数的类型、const、引用等属性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    template<typename F,typename T1,typename T2>
    void flip1(F f,T1 t1,T2 t2){
    f(t1,t2);
    }
    当传递一般的函数f或参数时,flip1一般能正常工作。但是传递的f函数是下面这样时,就得不到想要的结果。
    void f(int v1,int &v2){
    cout<<v1<<" "<<++v2<<endl;
    }
    我们期望的结果是:
    f(42,i); //f能够改变实参i的值。
    但实际:
    flip1(f,42,j); //j实参不会被改变。
    使用std::forward()和右值引用可以解决这个问题:
    template<typename F,typename T1,typename T2>
    void flip1(F f,T1 &&t1,T2 &&t2){
    f(std::forward<T1>(t1),std::forward<T2>(t2));
    }

四种智能指针

shared_ptr

简介

从名字可以看出是一个共享指针,允许多个shared_ptr指针指向一个资源,shared_ptr内部会有一个计数,记录指向该资源的指针个数。当计数为0,就会自动释放资源。

使用场景

当需要频繁申请内存时,使用shared_ptr来管理内存,可以在创建对象时自动初始化资源,也能在生命周期结束时自动释放内存。

创建方式

1
2
3
shared_ptr<int> p=std::make_shared<int>(10);//推荐使用make_shared()方式。
shared_ptr<T> p=std::make_shared<T>();
shared_ptr<int> p(new int(10));

使用方式

1
2
3
shared_ptr<int> p1=std::make_shared<int>(10);
shared_ptr<int> p2=std::make_shared<int>(20);
p1=p2;//p2的资源计数会加1,p1资源计数会减1(若p1指向资源计数为0,则释放资源)

注意事项
1.不混合使用普通指针和智能指针

1
2
3
4
5
6
7
8
9
10
11
12
13
//有如下函数:
void process(shared_ptr<int> ptr){
使用ptr
}//离开作用域,ptr被销毁。

//给process()传递参数时,不能传递普通指针和临时shared_ptr。
int *x(new int(1024));
process(x);//错误,不能将int*转换为shared_ptr<int>
process(shared_ptr<int>(x));//能编译通过,但是内存会被释放
int j=*x;//未定义行为,x已经是一个空悬指针!
可以这样使用:
shared_ptr<int> p1=std::make_shared<int>(1024);
process(p1);

2.智能指针内部有一个get()函数,可以获取到原生指针。注意get()到的指针不要再初始化另一个智能指针。

1
2
3
4
5
6
shared_ptr<int> p(new int(1024));
int *q=p.get();
{
shared_ptr<int>(q);
}//程序块结束会释放掉内存。
int foo=*p;//访问了释放掉的内存。

3.get()返回的指针不要去delete、reset其他智能指针、初始化其他智能指针。
4.如果资源不是new申请到的,要注意给智能指针传递一个删除器。

5.不要两个指针相互引用,会造成内存泄漏,可以用weak_ptr解决。

weak_ptr

简介

这是一个弱指针,它必须跟shared_ptr结合来用,它指向shared_ptr所管理的对象,但是它不会导致资源的引用计数变化.

使用场景

使用shared_ptr会有循环引用的问题,可以用weak_ptr来解决这个问题。

使用方式

1
2
3
4
5
//方式一
weak_ptr<T> w(sp);
//方式二
weak_ptr<T> w;
w=p;//p可以是shared_ptr或weak_ptr。

注意事项
1.weak_ptr在使用时,它指向的shared_ptr可能已经释放了,可以使用前先调用lock()。(检查weak_ptr是否为空指针)

1
2
3
if(shared_ptr<int> np=wp.lock()){
...
}
unique_ptr

简介

与shared_ptr不同,在某个时刻只能有一个unique_ptr指向一个给定对象,当unique_ptr被销毁时,它所指向的对象也被销毁。

使用场景

如果不需要对资源进行共享,优先使用unique_ptr.

1
2
//要采用直接初始化形式。
unique_ptr<double> p(new int(42));

注意事项
1.不能拷贝初始化、不能拷贝

1
2
3
unique_ptr<string> p2(p1);//错误
unique_ptr<string> p3;
p3=p2;//错误

2.不能拷贝的规则有一个例外,返回一个将要被销毁的unique_ptr可以发生拷贝。

1
2
3
4
5
6
7
8
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone(int p){
unique_ptr<int> ret(new int(p));
return ret;
}
auto_ptr(已遗弃)

简介

跟unique_ptr有一些相似的特性,同一时刻只能指向一个对象。但是这个允许拷贝,unique_ptr不允许拷贝。在cpp11已经被遗弃。

实现一个shared_ptr智能指针

000000

参考:技术: C++ 智能指针实现

shared_ptr的线程安全性

C++11的四种强制类型转换

1.static_case(静态转换)

  • 主要执行非多态的转换操作,用于代替C中通常的转换操作。
  • 隐式转换都建议使用 static_cast 进行标明和替换。
  • 在有类型指针与void *之间转换,不能使用static_cast在有类型指针间转换。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 1. 使用static_cast在基本数据类型之间转换
    float fval = 10.12;
    int ival = static_cast<int>(fval); // float --> int

    // 2. 使用static_cast在有类型指针与void *之间转换
    int *intp = &ival;
    void *voidp = static_cast<void *>(intp); // int* --> void*
    long *longp = static_cast<long *>(voidp);

    // 3. 用于类层次结构中基类和派生类之间指针或引用的转换
    // 上行转换(派生类---->基类)是安全的
    CDerived *tCDerived1 = nullptr;
    CBase *tCBase1 = static_cast<CBase*>(tCDerived1);
    // 下行转换(基类---- > 派生类)由于没有动态类型检查,所以是不安全的
    CBase *tCBase2 = nullptr;
    CDerived *tCDerived2 = static_cast<CDerived*>(tCBase2); //不会报错,但是不安全

    // 不能使用static_cast在有类型指针内转换
    float *floatp = &fval; //10.12的addr
    //int *intp1 = static_cast<int *>(floatp); // error,不能使用static_cast在有类型指针内转换

2.dynamic_cast(动态转换)

  • 用于将一个父类的指针/引用转化为子类的指针/引用(下行转换)。
  • 基类必须要有虚函数,因为 dynamic_cast 是运行时类型检查,需要运行时类型信息,而这个信息是存储在类的虚函数表中。
    1
    2
    3
    4
    5
    6
    CBase *p_CBase = new CBase;  // 基类对象指针
    CDerived *p_CDerived = dynamic_cast<CDerived *>(p_CBase); // 将基类对象指针类型转换为派生类对象指针

    CBase i_CBase; // 创建基类对象
    CBase &r_CBase = i_CBase; // 基类对象的引用
    CDerived &r_CDerived = dynamic_cast<CDerived &>(r_CBase); // 将基类对象的引用转换派生类对象的引用

3.const_cast(常量转换)

  • 常量指针(或引用)与非常量指针(或引用)之间的转换。
  • cosnt_cast 是四种类型转换符中唯一可以对常量进行操作的转换符。
  • 去除常量性是一个危险的动作,尽量避免使用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int value = 100;
    const int *cpi = &value; // 定义一个常量指针
    //*cpi = 200; // 不能通过常量指针修改值

    // 1. 将常量指针转换为非常量指针,然后可以修改常量指针指向变量的值
    int *pi = const_cast<int *>(cpi);
    *pi = 200;

    // 2. 将非常量指针转换为常量指针
    const int *cpi2 = const_cast<const int *>(pi); // *cpi2 = 300; //已经是常量指针

    const int value1 = 500;
    const int &c_value1 = value1; // 定义一个常量引用

    // 3. 将常量引用转换为非常量引用
    int &r_value1 = const_cast<int &>(c_value1);

    // 4. 将非常量引用转换为常量引用
    const int &c_value2 = const_cast<const int &>(r_value1);

4.reinterpret_cast(不相关类型的转换)

  • 用在任意指针(或引用)类型之间的转换。
  • 能够将整型转换为指针,也可以把指针转换为整型或数组。
  • reinterpret_cast 是从底层对数据进行重新解释,依赖具体的平台,可移植性差。
  • 尽量不使用这个转换符,高危操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int value = 100;
    // 1. 用在任意指针(或引用)类型之间的转换
    double *pd = reinterpret_cast<double *>(&value);
    cout << "*pd = " << *pd << endl;

    // 2. reinterpret_cast能够将指针值转化为整形值
    int *pv = &value;
    int pvaddr = reinterpret_cast<int>(pv);
    cout << "pvaddr = " << hex << pvaddr << endl;
    cout << "pv = " << pv << endl;

    /*
    输出结果:
    *pd = -9.25596e+61
    pvaddr = 8ffe60
    pv = 008FFE60
    */

https://www.cnblogs.com/linuxAndMcu/p/10387829.html

列表初始化

  • 效率,列表初始化是在变量创建的时候就进行初始化,相比构造函数体内的赋值要节省一遍拷贝操作,能提高运行时的效率。
  • 类内有const变量要用列表初始化,而不能赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    using namespace std;

    class Student{
    public:
    Student(int a):birthday(a){//如果不用列表初始化,下面的birthday无法编译通过。
    cout<<birthday<<endl;
    }
    private:
    const int birthday;
    };

    int main(){
    Student s1(1);
    return 0;
    }
  • 类内有类对象(该对象没有默认构造函数)必须用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CAnimal{
public:
CAnimal(int weight) :m_weight(weight) {
}
int m_weight;
};

class CDog {
public:
CAnimal m_a;//CAnimal类没有默认构造函数,但m_a必须得被初始化呀,Cdog构造函数就可以对m_a进行列表初始化。
const int m_b;
CDog(int a, int b) : m_a(a),m_b(b) {
}
};
  • 初始化顺序是根据定义顺序来的,跟初始化列表中的顺序无关。

decltype作用以及与auto区别。

1.作用:用来获取数据类型。

1
2
int tempA = 2;
decltype(tempA) dclTempA;

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

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

持续更新~

欢迎与我分享你的看法。
转载请注明出处:http://taowusheng.cn/