智能指针介绍和一些常见面试会问的问题
1智能指针介绍
C++ 11中定义了unique_ptr、shared_ptr与weak_ptr三种智能指针(smart pointer),都包含在头文件中。智能指针可以对动态分配的资源进行管理,保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。
因为在C++中我们频繁使用堆内存,但是堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。我们使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题,使用智能指针能更好的管理堆内存。实际上我们这个智能指针即对象,因为对象在程序结束时会自动调用其析构,就不会发生内存泄漏了,这也是我们实现智能指针一个重要的基础思想。
对智能指针的描述
智能指针是对普通指针用面向对象的类进行的一个封装,这使得智能指针实质是一个对象,行为表现的却像一个指针
智能指针的智能就在于它能够在适当的时机安全的帮助你释放内存
智能指针主要是运用了RAII技术(也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象)
2unique_ptr
如名字所示,unique_ptr是个独占指针,C++ 11之前就已经存在,unique_ptr所指的内存为自己独有,某个时刻只能有一个unique_ptr指向一个给定的对象,不支持拷贝和赋值。
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>
void test()
{
std::unique_ptr<int> up1(new int(11)); // 无法复制的unique_ptr
// unique_ptr<int> up2 = up1; // err, 不能通过编译
std::cout << *up1 << std::endl; // 11
std::unique_ptr<int> up3 = std::move(up1); // 现在p3是数据的唯一的unique_ptr
std::cout << *up3 << std::endl; // 11
// std::cout << *up1 << std::endl; // err, 运行时错误,空指针
up3.reset(); // 显式释放内存
up1.reset(); // 不会导致运行时错误
// std::cout << *up3 << std::endl; // err, 运行时错误,空指针
std::unique_ptr<int> up4(new int(22)); // 无法复制的unique_ptr
up4.reset(new int(44)); // "绑定"动态对象
std::cout << *up4 << std::endl; // 44
up4 = nullptr; // 显式销毁所指对象,同时智能指针变为空指针。与up4.reset()等价
std::unique_ptr<int> up5(new int(55));
int *p = up5.release(); // 只是释放控制权,不会释放内存
std::cout << *p << std::endl;
// cout << *up5 << endl; // err, 运行时错误,不再拥有内存
delete p; // 释放堆区资源
return;
}
3shared_ptr
shared_ptr允许多个该智能指针共享“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。支持复制和赋值操作。
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>
void test()
{
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::cout << "cout: " << sp2.use_count() << std::endl; // 打印引用计数, 2
std::cout << *sp1 << std::endl; // 22
std::cout << *sp2 << std::endl; // 22
sp1.reset(); // 显示让引用计数减一
std::cout << "count: " << sp2.use_count() << std::endl; // 打印引用计数, 1
std::cout << *sp2 << std::endl; // 22
return;
}
除了上面出现的use_count和reset之外,还有unique返回是否是独占所有权(use_count 为 1),swap交换两个shared_ptr对象(即交换所拥有的对象),get返回内部对象(指针)几个成员函数。
make_shared 函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。当要用make_shared时,必须指定想要创建的对象的类型或者使用更为简洁的auto,如下:
// 指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5指向一个值初始化的int,值为0
shared_ptr<int> p5 = make_shared<int>();
// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = make_shared<int>(42); //p指向的对象只有p一个引用者
auto q(p); //p和q指向相同对象,此对象有两个引用者
4weak_ptr
weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。没有重载 *和 -> 但可以使用lock获得一个可用的shared_ptr对象
weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个share_ptr对象,且在所指对象内存已经无效时,返回指针空值nullptr。为什么要使用weak_ptr
weak_ptr解决shared_ptr循环引用的问题
注意:weak_ptr并不拥有资源的所有权,所以不能直接使用资源。 可以从一个weak_ptr构造一个shared_ptr以取得共享资源的所有权。
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>
void check(std::weak_ptr<int> &wp) {
std::shared_ptr<int> sp = wp.lock(); // 转换为shared_ptr<int>
if (sp != nullptr) {
std::cout << "still: " << *sp << std::endl;
} else {
std::cout << "still: " << "pointer is invalid" << std::endl;
}
}
void test()
{
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指对象
std::cout << "count: " << wp.use_count() << std::endl; // count: 2
std::cout << *sp1 << std::endl; // 22
std::cout << *sp2 << std::endl; // 22
check(wp); // still: 22
sp1.reset();
std::cout << "count: " << wp.use_count() << std::endl; // count: 1
std::cout << *sp2 << std::endl; // 22
check(wp); // still: 22
sp2.reset();
std::cout << "count: " << wp.use_count() << std::endl; // count: 0
check(wp); // still: pointer is invalid
return;
}
5nullptr
nullptr 出现的目的是为了替代 NULL
6类型推导Auto Decltype
Auto:变量类型推导
Decltype:返回值类型推导
拖尾返回类型、auto 与 decltype 配合
7模板增强(外部模板)
1,C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该编译文件中实例化模板
2,尖括号 “>”
std::vector<std::vector<int>> wow;
3,类型别名模板
C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:
template <typename T>
using NewType = SuckType<int, T, 1>; // 合法
4,默认模板参数
8构造函数(委托构造,继承构造)
1,委托构造
C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
2,继承构造
在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。
假若基类拥有为数众多的不同版本的构造函数,这样,在派生类中得写很多对应的“透传”构造函数。如下:
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
B(int i):A(i){}
B(double d,int i):A(d,i){}
B(folat f,int i,const char* c):A(f,i,e){}
//......等等好多个和基类构造函数对应的构造函数
};
C++11的继承构造:
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
using A::A;
//关于基类各构造函数的继承一句话搞定
//......
};
9Lambda 表达式
Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
lambda表达式的大致原理:
每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
Lambda 表达式的基本语法如下:
[ caputrue ] ( params ) opt -> ret { body; };
-
capture是捕获列表;
-
params是参数表;(选填)
-
opt是函数选项;可以填mutable,exception,attribute(选填)
mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
exception说明lambda表达式是否抛出异常以及何种异常。
attribute用来声明属性。 -
ret是返回值类型(拖尾返回类型)。(选填)
-
body是函数体。
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。 -
[]不捕获任何变量。
-
[&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
-
[=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
int a = 0;
auto f = [=] { return a; };
a+=1;
cout << f() << endl; //输出0
int a = 0;
auto f = [&a] { return a; };
a+=1;
cout << f() <<endl; //输出1
- [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
- [bar]按值捕获bar变量,同时不捕获其他变量。
- [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
10新增容器
std::forward_list
std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率。
无序容器
C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。
元组 std::tuple
元组的使用有三个核心的函数:
std::make_tuple: 构造元组
std::get: 获得元组某个位置的值
std::tie: 元组拆包
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "张三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成绩: " << grade << ", "
<< "姓名: " << name << '\n';
}
合并两个元组,可以通过 std::tuple_cat 来实现。
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
11正则表达式
正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:
- 检查一个串是否包含某种形式的子串;
- 将匹配的子串替换;
- 从某个串中取出符合条件的子串。
C++11 提供的正则表达式库操作 std::string 对象,对模式 std::regex (本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch (本质是 std::match_results 对象)。
我们通过一个简单的例子来简单介绍这个库的使用。考虑下面的正则表达式:
另一种常用的形式就是依次传入 std::string/std::smatch/std::regex 三个参数
12. 语言级线程支持
std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable
代码编译需要使用 -pthread 选项
13 右值引用和move语义
14 C++左值和右值
我们就可以稍微总结一下:
左值通常是:可以被修改的对象或者变量(有名的)
右值通常是:常量,const修饰的变量,表达式的返回值(临时对象)(临时和无名的)
1.普通左值引用只能引用左值
2.const左值引用可以引用右值
3.右值引用可以引用右值
4.右值引用可以引用move(左值)
例4: 右值引用 以及 move的作用
int main()
{
//1 右值引用引用右值
int &&e = 10; //10是常量,是右值。int&&c 就是定义右值c
//4.右值引用可以引用左值吗?? 不能
//但是可以用move将左值转换成右值,就可以用右值引用了。
int f = 0; // f是左值
e = f // Eor 不能的,编译报错
e = move(f); //可以
}
在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。
值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、“right value” 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 “read value”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。
1)可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用
2) 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
C++11中右值划分为两种:
1.纯右值: 常量和基本类型的临时变量。 比如 : 10 , a+b
2.将亡值:自定义类型的临时变量。比如string s1, string s2 , string tmp = s1+s2; return tmp。 tmp就是一个临时变量。
右值引用的用途 – 移动构造(重点)
C++11右值引用的提出,就是提出了移动拷贝和移动构造的概念。所谓的移动拷贝和移动构造,就是将函数返回的将亡值与要构造的值交换,这样这个将亡值就会交换到要构造的值上;要构造的值上的初始值交换到将亡值上去。 这个将亡值反正都是要进行处理,所以是什么值无所谓。这样就少了一次拷贝的过程,提高了效率。
总结一波:
C++11中,对于库中的每一个类会默认的生成一个右值引用的移动构造,移动构造只是交换了指针,所以是浅拷贝。当用户定义的类中有深拷贝,或者涉及资源管理,用户必须显式定义自己的移动构造。
总结一下:复制构造函数执行的是深度拷贝,因为源对象本身必须不能被改变。而转移构造函数却可以复制指针,把源对象的指针置空,这种形式下,这是安全的,因为用户不可能再使用这个对象了。
http://c.biancheng.net/cplus/11/
1.宏定义
2.右值引用
3.模板
左值可以取地址,右值没法取地址
15C++中函数重载和函数覆盖的区别
C++中经常会用到函数的重载和覆盖,二者也在很多场合都拿出来进行比较,这里我就对二者的区别做点总结:
函数重载:
函数重载指的是函数名相同、函数特征值不同的一些函数,这里函数的特征值指的是函数的参数的数目、参数类型和参数的排列顺序。当函数的参数数目、参数类型和参数的排列顺序都相同的时候就说明函数的特征值相同。
注:在函数重载的时候,仅仅函数的返回值不同是不行的,必须还包括函数的特征值不同才行,仅仅返回值的不同在调用函数的时候,程序依然不能分辨用户调用的是哪一个函数。
当运用函数重载时,函数名都是相同的,但是函数的特征值必须至少有一项是不同的,运用重载函数我们不必在意参数的变量名,只需要关注上述的三个特征值即可。当运用此函数的时候,程序会自动进行参数匹配,然后调用特征值匹配正确的一个函数。
函数覆盖:
函数的覆盖发生在父类与子类之间,其函数名、参数类型、返回值类型必须与父类中相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖的函数版本,这种机制就叫做函数覆盖。
16public protect private三种继承方式
public 继承
protect 继承
private 继承
组合结果
基类中 继承方式 子类中
public & public继承 => public
public & protected继承 => protected
public & private继承 = > private
protected & public继承 => protected
protected & protected继承 => protected
protected & private继承 = > private
private & public继承 => 子类无权访问
private & protected继承 => 子类无权访问
private & private继承 = > 子类无权访问
由以上组合结果可以看出
1、public继承不改变基类成员的访问权限
2、private继承使得基类所有成员在子类中的访问权限变为private
3、protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
4、基类中的private成员不受继承方式的影响,子类永远无权访问。
16菱形继承
在多重继承中,存在一个很特殊的继承方式,即菱形继承。比如一个类C通过继承类A和类B,但是类A和类B又同时继承于公共基类N。示意图如下:
这种继承方式也存在数据的二义性,这里的二义性是由于他们间接都有相同的基类导致的。 这种菱形继承除了带来二义性之外,还会浪费内存空间。
虚基类 & 虚基类
为了解决上述菱形继承带来的问题,C++中引入了虚基类,其作用是 在间接继承共同基类时只保留一份基类成员,虚基类的声明如下:
class A//A 基类
{ … };
//类B是类A的公用派生类, 类A是类B的虚基类
class B : virtual public A
{ … };
//类C是类A的公用派生类, 类A是类C的虚基类
class C : virtual public A
{ … };
虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式声明的。
虚继承是声明类时的一种继承方式,在继承属性前面添加virtual关键字。
虚基类的初始化
这里直接说明结论,对于虚基类的初始化是由最后的派生类中负责初始化。
在最后的派生类中不仅要对直接基类进行初始化,还要负责对虚基类初始化。
17 C++中的RAII介绍
摘要
RAII技术被认为是C++中管理资源的最佳方法,进一步引申,使用RAII技术也可以实现安全、简洁的状态管理,编写出优雅的异常安全的代码。
资源管理
RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。
内存只是资源的一种,在这里我们讨论一下更加广义的资源管理。比如说文件的打开与关闭、windows中句柄的获取与释放等等。按照常规的RAII技术需要写一堆管理它们的类,有的时候显得比较麻烦。但是如果手动释放,通常还要考虑各种异常处理,比如说:
void function()
{
FILE *f = fopen("test.txt", 'r');
if (.....)
{
fclose(f);
return;
}
else if(.....)
{
fclose(f);
return;
}
fclose(f);
......
}
这里介绍一个网上实现的我认为比较简洁的方法,文章在这里。作者使用了C++11标准中的lambda表达式和std::function相结合的方法,非常简洁、明了。直接看代码吧:
#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)
#define ON_SCOPE_EXIT(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)
class ScopeGuard
{
public:
explicit ScopeGuard(std::function<void()> f) :
handle_exit_scope_(f){};
~ScopeGuard(){ handle_exit_scope_(); }
private:
std::function<void()> handle_exit_scope_;
};
int main()
{
{
A *a = new A();
ON_SCOPE_EXIT([&] {delete a; });
......
}
{
std::ofstream f("test.txt");
ON_SCOPE_EXIT([&] {f.close(); });
......
}
system("pause");
return 0;
}
作者为了使用方便,还定义了根据行号来对ScopeGuard类型对象命名的宏定义。看到了吧,当ScopeGuard对象超出作用域,ScopeGuard的析构函数中会调用handle_exit_scope_函数,也就是lambda表达式中的内容,所以在lamabda表达式中填上资源释放的代码即可,多么简洁、明了。既不需要为每种资源管理单独写对应的管理类,也不需要考虑手动释放出现各种异常情况下的处理,同时资源的申请和释放放在一起去写,永远不会忘记。
状态管理
RAII另一个引申的应用是可以实现安全的状态管理。一个典型的应用就是在线程同步中,使用std::unique_lock或者std::lock_guard对互斥量std:: mutex进行状态管理。通常我们不会写出如下的代码:
std::mutex mutex_;
void function()
{
mutex_.lock();
......
......
mutex_.unlock();
}
因为,在互斥量lock和unlock之间的代码很可能会出现异常,或者有return语句,这样的话,互斥量就不会正确的unlock,会导致线程的死锁。所以正确的方式是使用std::unique_lock或者std::lock_guard对互斥量进行状态管理:
std::mutex mutex_;
void function()
{
std::lock_guard<std::mutex> lock(mutex_);
......
......
}
在创建std::lock_guard对象的时候,会对std::mutex对象进行lock,当std::lock_guard对象在超出作用域时,会自动std::mutex对象进行解锁,这样的话,就不用担心代码异常造成的线程死锁。
18 C++类里面的成员函数有哪些?
1构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
-
函数名与类名相同。
-
无返回值。
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数可以重载。
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定
义编译器将不再生成。 -
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
-
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
-
成员变量的命名风格
2析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的资源清理工作。
析构函数是特殊的成员函数。
其特征如下:
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值。
-
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
-
对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。
3拷贝构造函数
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
拷贝构造函数也是特殊的成员函数,其特征如下:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
-
若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
date类:使用编译器默认生成的拷贝构造函数和赋值运算符重载,没有任何问题。
seqlist/string:使用编译器生成的拷贝构造函数和赋值运算符重载:浅拷贝(值的拷贝—字节方式拷贝)将一个对象的内容原封不动的拷贝到另一个对象中。
如果该类中未涉及到资源管理时,使用浅拷贝不会出现任何问题,否则:代码将会崩溃(多个对象在底层使用同一份资源,在对象销毁时会导致同一份资源释放多次而引起程序崩溃)
4.深浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。要解决浅拷贝问题,C++中引入了深拷贝。
深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。
//浅拷贝
class String
{
public:
String(const char* str)
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
String(const String& s)
:_str(s._str)
{}
String& operator=(const String& s)
{
if (this != &s)
{
_str = s._str;
}
return *this;
}
~String()
{
if (_str)
{
delete[] _str;
}
_str = NULL;
}
private:
char* _str;
};
//深拷贝:
class String
{
public:
String(const char* str)
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
String(const String& s)
{
_str=new char[strlen(s)+1];
strcpy(_str,s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
strcpy(_str,s._str);
}
return *this;
}
~String()
{
if (_str)
{
delete[] _str;
}
_str = NULL;
}
private:
char* _str;
};
4赋值操作符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5…* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符主要有四点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
5const成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
-
const对象可以调用非const成员函数吗? 不能
-
非const对象可以调用const成员函数吗? 能
-
const成员函数内可以调用其它的非const成员函数吗? 不能
-
非const成员函数内可以调用其它的const成员函数吗? 能
6取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};