C++笔记-3
C/C++ 中 volatile 关键字详解
volatile用于随时可能发生变化的变量,避免因为编译器优化程序而出错。编译器在多次访问一个变量时,并不会一直通过寻址访问,而是访问寄存器中已经存好的副本(因为快),这时如果出现一些编译器预料之外的改动就会出错。如内嵌汇编操纵栈”、多线程并发访问共享变量时,一个线程改变了变量的值,其他线程可能看不见改动。因此volatile 的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:
链接:https://www.runoob.com/w3cnote/c-volatile-keyword.html
1 |
|
加了关键字后每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读到寄存器(或高速缓存)中的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。加入volatile int i = 10;第二次i=32,符合预期
C/C++ 中 左值、右值、右值引用
链接https://nettee.github.io/posts/2018/Understanding-lvalues-and-rvalues-in-C-and-C/
1 |
|
main.cpp:
1 |
|
右值引用:
1 |
|
- Intvec& operator=(const Intvec& other)如果不加const,那么右值Intev(33)传不进去,因为引用是左值,但右值和常量左值可以绑定。去掉const会报错。不用右值引用的方法是把传进来的右值赋值给左值,再swap()。
- 在C++11之后,swap()中可以传右值引用了。
- 在不能用右值引用的情况下想把右值传进函数Intvec& operator=(const Intvec& other),只能在传参处加const,利用常量左值可以绑定右值把右值传进来,然后在按照右值做一个左值副本传进swap()。为什么不能直接向swap()内传对象other?因为swap()内不能传const对象,多以必须建一个副本。可见C++11引入右值引用就完全解决了这个问题。
std::move()
链接:https://blog.csdn.net/p942005405/article/details/84644069
在C++11中,标准库在
从实现上讲,std::move基本等同于一个类型转换:static_cast
C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 因此在push_back()中定义了通过传入右值来执行移动构造函数来节省内存,通过std::move创建对象的右值,可以避免不必要的拷贝操作。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
对于内置类型(如 int、float、double 等),它们的复制和移动本质上是相同的,因为它们的值都是在栈上分配的,没有动态资源需要管理。因此,即使使用 std::move() 将其转换为右值引用,实际上也只是将其视为右值,但不会导致任何性能上的提升。
性能提升通常是与管理动态资源相关的。例如,在使用自定义类型、STL容器或者智能指针等情况下,它们通常会涉及动态内存的分配和释放,以及资源的所有权转移。在这种情况下,使用 std::move() 可以显著提升性能,因为移动操作只是简单地将资源的所有权从一个对象转移到另一个对象,而不需要进行深层的复制操作。这样可以避免额外的内存分配和释放,从而提高程序的性能。
1 |
|
emplace_back()和push_back()
链接:https://zhuanlan.zhihu.com/p/213853588
二者的主要区别在于对于存放右值对象时的不同。C++11之后,二者仅在一种情况下存在区别:
1 |
|
二者唯一的区别如下:
1 |
|
剩下的情况:
- 向vector中放入左值(对象):person.push_back(p);person.emplace_back(p);都是调用构造函数,拷贝构造函数。
- 向vector中放入右值(临时对象)person.push_back(Person(1));person.emplace_back(Person(1));都是调用构造函数,移动构造函数。
- 向vector中放入右值(临时对象)person.push_back(std::move(p));person.emplace_back(std::move(p));和上面一样,都是调用构造函数,移动构造函数,因为都是传了一个右值。std::move()将传入的左值强制转换为右值,如string类型。但自己构造的类对象,基本类型则不会。
1 |
|
在上面例子中,注意加入第二个对象后,vector会扩容,原先就有的p是移动构造到更大的vector中的。将上面例子中emplace_back换为push_back结果是一样的,这是因为C++11后,push_back()和emplace_back()都对传入右值做了设计,唯一有区别的地方就是直接传入对象参数时emplace_back就地构造,而不是像push_back要构造再移动构造。剩余情况而是都是要先构造,再复制/移动构造。
除此之外,如果设计的类中没有设计移动构造,那么原本要移动构造可以向下兼容成拷贝构造。
这里提供一个调试的demo:
1 |
|
Const修饰变量、成员函数、返回值
链接:https://zhuanlan.zhihu.com/p/110159656
1 |
|
- const a调用不了没被const修饰的成员函数,要在int& get_data()后加const。
- non-const a可以调用int& get_data() const
下面讲解实际原理
链接:https://blog.csdn.net/weixin_45031801/article/details/133906995
链接:https://blog.csdn.net/weixin_45031801/article/details/134161230
1 |
|
- 成员函数实际上隐藏了this指针,void Printf()实际上是void Printf(Date const this);Date(int year, int month, int day)实际上是Date(Date const this,int year, int month, int day),也就是指针指向是不可改变的(永远指向自己)。在用户调用时d1.Print();实际上是d1.Print(&d1);简而言之,把自己的指针传了进去。这些都是编译器要做的部分。
- 发生错误原因:const对象无法调用non-const成员函数。究其本质为const Data 转化为 Data const是存在歧义的。解决方法是把void Printf( Date const this) 变为 void Printf(const Date const this),但由于this指针是隐含的,我们默许不让this出现在成员函数之中,因此经典的const成员函数出现了:
void Print() const 等价于void Print(const Date* const this)。权限缩小,可以。权限扩大,不行。 - const成员函数不可以调用非const成员函数,void Fun( const Date& d){d.printf()};非const成员函数可以调用其他const成员函数。
static修饰变量与函数
链接:https://blog.csdn.net/weixin_45031801/article/details/134215425?spm=1001.2014.3001.5502
static修饰局部变量
局部静态变量也是放在静态区(和全局变量放在一起)。生命周期为整个程序周期,并且不用赋值,默认为0。
static修饰全局变量
全局变量的作用域十分的广,只要在一个源文件中定义后,这个程序中的所有源文件、对象以及函数都可以调用,生命周期更是贯穿整个程序。文件中的全局变量想要被另一个文件使用时就需要进行外部声明(以下用extern关键字进行声明)。——-也即是说全局变量既可以在源文件中使用,也可以在其他文件中使用(只需要使用extern外部链接以下即可)
static修饰全局变量和函数时,会改变全局变量和函数的链接属性———-变为只能在内部链接,从而使得全局变量的作用域变小。如果全局变量被static修饰,那这个外部链接属性就会被修改成内部链接属性,此时这个全局变量就只能在自己的源文件中使用;
- static的另一个作用是默认初始化为0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。
- 存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。之后再次运行到含有 static 关键字的初始化语句时不会再执行该语句。共有两种变量存储在静态存储区:全局变量和 static 变量,只不过和全局变量比起来,static 可以控制变量的可见范围。
C++中的static
1 |
|
- 这里A的大小是8,这是因为静态成员变量不会每个对象都创建一个,而是所有对象共享类中声明的这一个,其作用域限制在类之内。
- 静态成员要在类外定义,定义时不添加static关键字
- 静态成员函数没有隐藏的this指针,没有向函数中传this指针也就不能访问任何非静态成员
1 |
|
当静态成员变量为public时,可有如下三种进行访问:通过对象.静态成员来访问;通过类名::静态成员来行访问;通过匿名对象突破类域进行访问
1 |
|
当静态成员变量变成private时,可采用如下方式:通过对象.静态成员函数来访问;通过类名::静态成员函数来行访问;通过匿名对象调用成员函数进行访问
1 |
|
注意:
- 静态成员函数不可以调用非静态成员函数吗?因为静态成员函数是没有this指针的,无法调用非静态成员函数。
- 非静态成员函数可以调用类的静态成员函数吗。因为静态成员为所有类对象所共享,不受访问限制
linux系统下的一些权限问题
用户权限、所有组、更改权限:https://blog.csdn.net/weixin_45031801/article/details/133876760?spm=1001.2014.3001.5502
sudo存在的意义:https://zhuanlan.zhihu.com/p/137332644
利用引用初始化类,并且默认拷贝构造函数, copy assignment operator复制问题
1 |
|
同样要注意,如果是通过Person p1(p2);调用拷贝构造函数,string&给string&赋值的操作其实会调用string()的构造函数给p1中的string&初始化。
派生类的拷贝构造
1 |
|
- 派生类的任何构造函数都要带基类的拷贝构造函数,否则会调用拷贝构造函数。
- Person1(const Person1& p):Person(p.get_name())中get_name传的this指针是const Person* const类型,因此要求get_name()必须是const修饰的成员函数。
- 实例化一个派生类时肯定会附带生成一个基类。
- 拷贝构造函数中传的必须是const类型引用,pass-by-value会无限调用自身,不加const会导致用户传入const实参时会出现权限(范围)冲突。
- 纯虚函数定义部分用=0代替,=0告诉编译器函数没有主体,改函数是纯虚函数
- =delete,C++11中,当我们定义一个类的成员函数时,如果后面使用”=delete”去修饰,那么就表示这个函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则就会出错。
对于构建的类,编译器默认生成的函数
链接:https://zhuanlan.zhihu.com/p/111652531
在C++中定义一个空类 class Dog {};
1 |
|
生成规则:
- 默认构造函数:只在用户没有定义其它类型构造函数时才会生成。
- 拷贝构造函数:只有用户没有定义 移动构造函数(5) 和 移动赋值运算符(6) 时,才会生成。
- 拷贝赋值运算符:只有用户没有定义 移动构造函数(5) 和 移动赋值运算符(6) 时,才会生成。
- 析构函数:无限制。
- 移动构造函数:只有用户没有定义 拷贝构造函数(2), 拷贝赋值运算符(3), 析构函数(4) 和 移动赋值运算符(6) 时,才会生成。
- 移动赋值运算符:只有用户没有定义 拷贝构造函数(2), 拷贝赋值运算符(3), 析构函数(4) 和 移动构造函数(5) 时,才会生成。
注意一个类class X
X x1;std::move(x1);这样输出x1中的值不会有任何影响。
但X x1; X x2 = std::move(x1);这样输出x1中的值就被转移走了。
1 |
|
C++ Lambda表达式
链接:https://zhuanlan.zhihu.com/p/384314474
链接:https://xiaodongfan.com/
C++ static、const 类型成员变量声明以及初始化,constexpr,常量表达式与字面值
声明时初始化、初始化列表初始化、构造函数中初始化
成员变量初始化的顺序为:先进行声明时初始化,然后进行初始化列表初始化,最后进行构造函数初始化。
1 |
|
- static成员变量不能在构造函数初始化列表中初始化,因为它不属于某个对象。定义必须在类的外部实现。其在构造函数中是赋值行为。
- const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象没被创建时,编译器不知道 const 数据成员的值是什么。
const 数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt。
常量表达式,const,constexpr,编译时常量,运行时常量
链接:https://blog.csdn.net/aruewds/article/details/118938121
链接:https://xiaodongfan.com/C-%E5%B8%B8%E9%87%8F%E8%A1%A8%E8%BE%BE%E5%BC%8F-constexpr.html
链接:https://www.runoob.com/w3cnote/cpp-static-const.html
链接:https://blog.csdn.net/gyxqwe/article/details/78587078
链接:https://blog.csdn.net/yao_hou/article/details/109301290
字面值:是一个不能改变的值,如数字、字符、字符串等。单引号内的是字符字面值,双引号内的是字符串字面值。
字面值类型(literal type):算数类型、引用和指针等。
常量表达式(const experssion):是指(1)值不会改变 并且 (2)在编译过程就能得到计算结果的表达式。字面量属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。
1 |
|
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的是否是一个常量表达式。
声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
const与constexpr区别
const 和 constexpr 变量之间的主要区别在于:const 变量的初始化可以延迟到运行时,而 constexpr 变量必须在编译时进行初始化。所有 constexpr 变量均为常量,因此必须使用常量表达式初始化。
在使用const时,如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针本身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
与const不同,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。constexpr是一种很强的约束,更好的保证程序的正确定语义不被破坏;编译器可以对constexper代码进行非常大的优化,例如:将用到的constexpr表达式直接替换成结果, 相比宏来说没有额外的开销。
1 |
|
用于引用
1 |
|
简单的说constexpr所引用的对象必须在编译期就决定地址。还有一个奇葩的地方就是可以通过上例conexprPtrD来修改g_tempA的值,也就是说constexpr修饰的引用不是常量,如果要确保其实常量引用需要constexpr const来修饰。内因类似于constexpr修饰指针即要求指针自身是常量,并不要求所指内容是常量,并且g_tempA自身的地址在编译期就可以确定,因此可以通过conexprPtrD来修改g_tempA的值。
C++ 智能指针auto_ptr unique_ptr share_ptr weak_ptr
链接:https://blog.csdn.net/cpp_learner/article/details/118912592
auto_ptr unique_ptr
auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有三大问题:
- 复制和赋值会改变资源的所有权,不符合人的直觉(下面的利用右值移动拷贝/赋值显然更符合人的直觉)。
- 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
- 不支持对象数组的操作。
所以,C++11用更严谨的unique_ptr 取代了auto_ptr!unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。
- 基于排他所有权模式:两个指针不能指向同一个资源。
- 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值。
- 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
- 在容器中保存指针是安全的(但也要用std::move()传右值)。
智能指针三大接口:
- get() 获取智能指针托管的指针地址
1 |
|
- release() 取消智能指针对动态内存的托管
1 |
|
也就是智能指针不再对该指针进行管理,改由管理员进行管理!对象依旧存在。
- reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
1 |
|
reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之,然后智能指针就会托管参数的那个指针了。没有参数,则将其释放掉。
auto_ptr 与 unique_ptr智能指针的内存管理陷阱
1 |
|
为了解决这两种指针的排他性,由此引出share_ptr。
share_ptr
为什么make_shared效率更高?
链接:https://zhuanlan.zhihu.com/p/337656226
shared_ptr中维护了两个指针,两个内存块,一个是目标对象的指针,对象内存块(我们称被指对象为data_field,定义了share_ptr后根据构造函数创建),另一个是包含了use_count,weak_count,allocator、deletor的内存块(我们称被指的内容为control_block,这一部分是定义了share_ptr后才创建的),以及指向这一块的指针。通常data field和control_block是分离的,比如当我们通过如下代码创建一个shared_ptr时:
1 |
|
首先,我们为int类型new出了一块内存,然后在构造__shared_count的时候,又会为control block new出一块内内存,这是两次内存分配,内存分配对系统来说消耗极大,并且产生了内存碎片。
如果使用std::shared_ptr,那么会把这两块挨着放在一起,这是一次内存分配。
shared_ptr的问题:循环引用
问题参考链接:https://blog.csdn.net/cpp_learner/article/details/118912592
为了解决这个问题,引入了weak_ptr:
weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。
C时代的遗孤,char* p=”123”和char p[]=”123”的区别
char[] 是数组类型, char 是指针类型。两者根本上是不同的东西。
不同点:
const char初始化”123”,由于”123”是字符串常量,存储在可执行文件的只读数据段,所以是指针p指向了字符串常量。
初始化后者时,字符串p是将字面量的值复制到栈里,命名为p。
1 |
|
- char[] 能隐式转化为char,char可以隐式转换为string,
- char p=”123”实则会报warning,实际应该是const char p =”123”,因此p[0]=’b’是不对的。
- string p = “123”,p[0]=’b’是对的,可以改变。
内联函数
链接:https://blog.csdn.net/qq_35902025/article/details/127912415
野指针的产生
指向非法的内存地址指针叫作野指针(Wild Pointer),也叫悬挂指针(Dangling Pointer),意为无法正常使用的指针。
链接:https://blog.csdn.net/Hsuesh/article/details/111212006
使用未初始化的指针
出现野指针最典型的情形就是在定义指针变量之后没有对它进行初始化,如下面的程序:
1 |
|
指针所指的对象已经消亡
指针指向某个对象之后,当这个对象的生命周期已经结束,对象已经消亡后,仍使用指针访问该对象,将出现运行时错误。考察如下程序。
1 |
|
指针释放后之后未置空
指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。对指针进行free和delete,只是把指针所指的内存空间给释放掉,但并没有把指针本身置空,此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生野指针。考察如下程序。
1 |
|
- warning: cast to pointer from integer of different size [-Wint-to-pointer-cast] cout<<(void)(void*)(&b)<<”\t”<<(int64_t)(int64_t)(&b)<<”\t”<<(int)(int)(&b)<<”\t”<<(int)a;这行代码中,指针都是八字节的,各种可以任意转换,问题出现在(int)(&b),(int)(&b)的意图是取出(int*)(&b)位置存的指针,结果对该位置解引用,解出来的应当八字节的指针而现如今是4字节的int,这是出现在64位机上的错误,八字节的指针被截断成了四字节。而由于32位机指针是4字节32位的,上面的代码是没有问题。这也是为什么把int换成int64_t在64位机上是完全没有问题的。
- 虚表中,虚析构函数附带着一个未知的函数,相当于虚析构函数占两条,其他虚函数占一个。
- 由于对void 解引用没有作用,技巧:直接把void 强转为(void ),void 可以解引用,强转为void n*也可以,都是指针罢了,打印出来都一样。
- 类的实例地址就是指向虚表的指针,一路解引用下去就是指向第一个虚函数的指针。取出该指针后面直接加()就可以调用
- 对于静态函数和普通函数,函数指针可以直接转化为(void)然后cout打印出函数指针,静态函数的函数签名在本例中就是void( )(void);但对于普通成员函数,例如Base::ffuc() 是一个成员函数指针,类型是 void (Base:: )() ,它与普通函数指针 void ( )() 是不兼容的。因此,不能直接将成员函数指针转换为 void,因为两者的内部表示不同。编译器不知道如何将一个带有 this 指针的成员函数指针转换为简单的 void。(直接打印函数指针而不强转,cout出来都是1)