C++知识点

static关键字

C\C++static关键字的用法总结
普通变量:
静态变量只初始化一次,未初始化的静态变量会默认初始化为0。静态全局变量只在本文件可见,外部文件无法访问。而静态局部变量只在定义的作用域内可见,但他们的生存周期都是整个程序运行时期。
静态函数主要是起到函数的隐藏作用,static修饰的函数只允许在当前文件中使用,在其他文件中无法找到该函数的地址,比如在一个工程中创建两个.cpp(.c)文件,main1、main2,在main1中定义一个static int add()函数并对该函数进行实现,而在main2中使用add函数是就编译会报错误,这就是由于static修饰该函数只允许在main1文件中使用造成的。

类:
static修饰的数据成员必须在类外进行初始化且只会初始化一次。

static修饰成员方法
static修饰的成员方法为静态成员方法,静态成员方法可以在类内或类外定义,但必须在类内声明;static成员方法没有this指针,所以不能直接引用非static数据成员或调用类的非static成员方法,只能调用类的static成员数据和static成员方法;static成员不是任何对象的组成,不依赖对象的调用所以static成员方法不能被声明为const,因为const只限定该类的对象;static成员方法不能同时被声明为虚函数。

重载,重写和覆盖

重载是指参数列表不同,函数名字相同,在同一类内的;
重写是指派生类继承基类的虚函数,参数列表一定相同;
覆盖是指派生类的方法与基类的方法名称相同,基类方法被隐藏。;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。

const对重载的影响,必须用指针或引用

通过值传递时,形参不会改变实参,因此const不能当做重载;
只有通过引用或指针的const才可以做重载。
返回值不能作为区分重载的条件。
const 对象只能调用const 方法,非const 对象既能调用const 方法也能调用非const方法,优先调用非const方法。

参考

虚函数

虚函数很棒一个链接
定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
虚函数只能借助于指针或者引用来达到多态的效果。
同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

假设我们有下面的类层次:

class A  
{  
public:  
    virtual void foo()  
    {  
        cout<<"A::foo() is called"<<endl;  
    }  
};  
class B:public A  
{  
public:  
    void foo()  
    {  
        cout<<"B::foo() is called"<<endl;  
    }  
};  
int main(void)  
{  
    A *a = new B();  
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!  
    return 0;  
}  

这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果。
C++纯虚函数
一、定义 
**纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtual void funtion1()=0
二、引入原因  
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性
。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。**这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
抽象类的介绍
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。(1)抽象类的定义: 称带有纯虚函数的类为抽象类。(2)抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。(3)使用抽象类时注意:**抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。**如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。抽象类是不能定义对象的。
总结:
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为:error LNK: unresolved external symbol “public: virtual void __thiscall ClassName::virtualFunctionName(void)”
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的

原因:
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。定义纯虚函数就是为了让基类不可实例化化因为实例化这样的抽象数据结构本身并没有意义。或者给出实现也没有意义实际上我个人认为纯虚函数的引入,是出于两个目的1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。2、为了效率,不是程序执行的效率,而是为了编码的效率。

函数指针

函数指针
声明:

void (*funP)(int) ;   //声明一个指向同样参数、返回值的函数指针变量。
void (*funA)(int);

funP=&myFun;  //将myFun函数的地址赋给funP变量
(*funP)(200);  //通过函数指针变量来调用函数

//myFun与funA的类型关系类似于int 与int 的关系。
funA=myFun;
funA(300);

由上可知,函数指针赋值可以采用 x = &fun, x = fun两种形式。

fork exec 多进程(fork写时复制)

参考链接
Linux下进程的结构
Linux下一个进程在内存里有三部分的数据,就是"代码段"、“堆栈段"和"数据段”。
“代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。"堆栈段"存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

fork函数
fork函数运行后,fork函数启动一个新的进程,前面我们说过,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是**,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了**,也就是说,它们之间不再共享任何数据了**。它们再要交互信息时,只有通过进程间通信来实现**,这将是我们下面的内容。既然它们如此相象,系统如何来区分它们呢?这是由函数的返回值来决定的。对于父进程, fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。

在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

写时复制是一种采取了惰性优化方法来避免复制时的系统开销。它的前提很简单:如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。

写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。

exec( )函数族
一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。

结合fork与exec的使用,实现启动另一程序的执行但自己仍想继续运行。

管道

管道是进程间通信中最古老的方式,它包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。

无名管道由pipe()函数创建:

#include <unistd.h>
int pipe(int filedis[2])

参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。下面的例子示范了如何在父进程和子进程间实现通信。
写之前关闭读,读之前关闭写

 1#define INPUT 0
 2 #define OUTPUT 1
 3 
 4 void main() {
 5    int file_descriptors[2];
 6    /*定义子进程号 */
 7    pid_t pid;
 8    char buf[256];
 9    int returned_count;
10    /*创建无名管道*/
11    pipe(file_descriptors);
12    /*创建子进程*/
13    if((pid = fork()) == -1) {
14       printf("Error in fork\n");
15       exit(1);
16    }
17    /*执行子进程*/
18    if(pid == 0) {
19       printf("in the spawned (child) process...\n");
20       /*子进程向父进程写数据,关闭管道的读端*/
21       close(file_descriptors[INPUT]);
22       write(file_descriptors[OUTPUT], "test data", strlen("test data"));
23       exit(0);
24    } else {
25       /*执行父进程*/
26       printf("in the spawning (parent) process...\n");
27       /*父进程从管道读取子进程写的数据,关闭管道的写端*/
28       close(file_descriptors[OUTPUT]);
29       returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
30       printf("%d bytes of data received from spawned process: %s\n",
31       returned_count, buf);
32    }
33 }

共享内存,信号量,信号

Linux进程间通信——使用信号量
Linux进程间通信——使用信号

智能指针,待续上,未看

参考链接
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

1,你知道智能指针吗?智能指针的原理。
2,常用的智能指针。
3,智能指针的实现。

1答案**:智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放**,

 2, 最常用的智能指针: 

          1)std::auto_ptr,有很多问题。 **不支持复制(拷贝构造函数)和赋值**(operator =),但复制或赋值的时候不会提示出错。因为不能被复制,所以**不能被放入容器中**。

          2) C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。

           例如:

                std::unique_ptr<int> p1(new int(5));
                std::unique_ptr<int> p2 = p1; // 编译会出错
                std::unique_ptr<int> p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针.

          3) C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。

          4)C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

Q:讲讲智能指针吧。
A:三种智能指针,shared_ptr,unique_ptr,weak_ptr,auto_ptr。

Q:shared_ptr的原理
A:shared_ptr维护了一个指向control block的指针对象,来记录引用个数。

Q:weak_ptr的原理
A:weak_ptr用于避免shared_ptr相互指向产生的环形结构,造成的内存泄漏。weak_ptr count是弱引用个数;弱引用个数不影响shared count和对象本身,shared count为0时则直接销毁。

Q:如何判断weak_ptr的对象是否失效?
A:1、expired():检查被引用的对象是否已删除。
2、lock()会返回shared指针,判断该指针是否为空

3、use_count()也可以得到shared引用的个数,但速度较慢

Q:shared 和 unique区别
A:unique具有唯一性,对指向的对象值存在唯一的unique_ptr。unique_ptr不可复制,赋值,但是move()可以转换对象的所有权,局部变量的返回值除外。与shared_ptr相比,若自定义删除器,需要在声明处指定删除器类型,而shared不需要,shared自定义删除器只需要指定删除器对象即可,在赋值时,可以随意赋值,删除器对象也会被赋值给新的对象。

Q:原因是什么?
A:unique的实现中,删除器对象是作为unique_ptr的一部分,而shared_ptr,删除器对象保存在control_block中。

new malloc

参考链接
c语言几种分配内存方式

malloc:
void *malloc(unsigned int size)
开辟一块长度为size的连续内存空间,返回类型为void类型的指针。在使用malloc开辟一段空间时,void*要显示的转换为所需要的类型,如果开辟失败,则返回NULL指针。

calloc
void* calloc (size_t num, size_t size);
开辟一块num个大小为size的连续空间,并将每一块空间初始化为0。

realloc
void *realloc(void *ptr,size_t size);
将内存地址为ptr的一段空间的大小调整为size大小。

如果ptr这段空间后面有足够的空间,就直接追加上来
如果ptr后面的空间不足,则在堆上重新开辟一块合适大小的连续空间,将原有数据拷贝到新的内存空间中,释放掉原来的内存,最后返回的则是新地址。

free
void free (void* ptr);
free函数是来释放动态开辟的内存的。
malloc和free要配套使用,如果没有free则会造成内存泄漏。

  • malloc/free是标准库函数,new/delete是C++运算符
  • malloc失败返回空,new失败抛异常
  • new/delete会调用构造、析构函数,malloc/free不会,所以他们无法满足动态对象的要求
  • new返回有类型的指针,malloc返回无类型的指针
    其他区别:
    1.分配内存的位置
    malloc是从堆上动态分配内存,new是从自由存储区为对象动态分配内存。
    自由存储区的位置取决于operator new的实现。自由存储区不仅可以为堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

2.返回类型安全性
malloc内存分配成功后返回void*,然后再强制类型转换为需要的类型;new操作符分配内存成功后返回与对象类型相匹配的指针类型;因此new是符合类型安全的操作符。

3.内存分配失败返回值
malloc内存分配失败后返回NULL;new分配内存失败则会抛异常(bac_alloc)。

如果你在函数上面定义了一个指针变量,然后在这个函数里申请了一块动态分配的内存让指针指向它。实际上,**这个指针的地址是在栈上,但是它所指向的内容却是在堆上面。**这一点要注意!所以,**在一个函数里申请了空间后,比如下面这个函数,函数返回,函数所在的栈被销毁指针也跟着销毁,申请的内存不会跟着销毁了!因为申请的内存在堆上。**所以,还是那句话:记得释放!

       void Function(void) 
       { 
        char *p = (char *)malloc(100 * sizeof(char)); 
    }

左值引用 右值引用

右值引用作用:移动语义 完美转发

移动语义
移动构造函数,移动赋值函数
完美转发
所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
myforward()函数能够将右值转发过去,但是并不能够转发左值,解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。例子如下:
参考链接

没看的一个很好的文章
总结
由两种值类型,左值和右值。
有三种引用类型,左值引用、右值引用和通用引用。左值引用只能绑定左值,右值引用只能绑定右值,通用引用由初始化时绑定的值的类型确定。
左值和右值是独立于他们的类型的,右值引用可能是左值可能是右值,如果这个右值引用已经被命名了,他就是左值。
引用折叠规则:所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。
移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值函数
std::move()将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数本身并没有对这个左值什么特殊操作。
std::forward()和universal references通用引用共同实现完美转发。
用empalce_back()替换push_back()增加性能。

C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
右值分为将亡值和纯右值。纯右值就是c++98标准中右值的概念,如非引用返回的函数返回的临时变量值;一些运算表达式,如1+2产生的临时变量;不跟对象关联的字面量值,如2,‘c’,true,“hello”;这些值都不能够被取地址。

而将亡值则是c++11新增的和右值引用相关的表达式,这样的表达式通常时将要移动的对象、T&&函数返回值、std::move()函数的返回值等,

int i=0;// i是左值, 0是右值

class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
A a = getTemp();   // a是左值  getTemp()的返回值是右值(临时变量)

区别

c++98中的引用很常见了,就是给变量取了个别名,在c++11中,因为增加了右值引用(rvalue reference)的概念,所以c++98中的引用都称为了左值引用(lvalue reference)。

int a = 10; 
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用

int& b = 1; //编译错误! 1是右值,不能够使用左值引用

c++11中的右值引用使用的符号是&&,如

int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
A && a = getTemp();   //getTemp()的返回值是右值(临时变量)

getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。

所以,左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。但是,常量左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

const int & a = 1; //常量左值引用绑定 右值, 不会报错

class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
const A & a = getTemp();   //不会报错 而 A& a 会报错

总结一下,其中T是一个具体类型:

左值引用, 使用 T&, 只能绑定左值
右值引用, 使用 T&&, 只能绑定右值
常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
已命名的右值引用,编译器会认为是个左值
编译器有返回值优化,但不要过于依赖

移动构造和移动赋值

右值通过右值引用,左值通过std::move()

右值(如局部对象)的移动构造

用c++实现一个字符串类MyString,MyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享,不然一个析构了,另一个也就完蛋了。
具体代码:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
//    static size_t CCtor; //统计调用拷贝构造函数的次数
public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << MyString::CCtor << endl;
}

代码看起来挺不错,却发现执行了1000次拷贝构造函数,如果MyString(“hello”)构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString(“hello”)只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。而C++11新增加的移动语义就能够做到这一点。
要实现移动语义就必须增加两个函数:移动构造函数和移动赋值构造函数

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
    static size_t MCtor; //统计调用移动构造函数的次数
    static size_t CAsgn; //统计调用拷贝赋值函数的次数
    static size_t MAsgn; //统计调用移动赋值函数的次数

public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 移动构造函数
   MyString(MyString&& str) noexcept
       :m_data(str.m_data) {
       MCtor ++;
       str.m_data = nullptr; //不再指向之前的资源了
   }

   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       CAsgn ++;
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   // 移动赋值函数 =号重载
   MyString& operator=(MyString&& str) noexcept{
       MAsgn ++;
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = str.m_data;
       str.m_data = nullptr; //不再指向之前的资源了
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 结果
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString(“hello”)是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。
在这里插入图片描述

左值的移动构造

提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        MyString tmp("hello");
        vecStr.push_back(tmp); //调用的是拷贝构造函数
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;

    cout << endl;
    MyString::CCtor = 0;
    MyString::MCtor = 0;
    MyString::CAsgn = 0;
    MyString::MAsgn = 0;
    vector<MyString> vecStr2;
    vecStr2.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        MyString tmp("hello");
        vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 运行结果
CCtor = 1000
MCtor = 0
CAsgn = 0
MAsgn = 0

CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/
MyString str1("hello"); //调用构造函数
MyString str2("world"); //调用构造函数
MyString str3(str1); //调用拷贝构造函数
MyString str4(std::move(str1)); // 调用移动构造函数、
//    cout << str1.get_c_str() << endl; // 此时str1的内部指针已经失效了!不要使用
//注意:虽然str1中的m_dat已经称为了空,但是str1这个对象还活着,知道出了它的作用域才会析构!而不是move完了立刻析构
MyString str5;
str5 = str2; //调用拷贝赋值函数
MyString str6;
str6 = std::move(str2); // str2的内容也失效了,不要再使用

需要注意一下几点:

  • str6 = std::move(str2),虽然将str2的资源给了str6,但是str2并没有立刻析构,只有在str2离开了自己的作用域的时候才会析构,所以,如果继续使用str2的m_data变量,可能会发生意想不到的错误。
  • 如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因
  • c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。
  • 更多请见
    更多包括:
    universal references(通用引用)
    完美转发
    emplace_back减少内存拷贝和移动,vector等容易用emplace_back代替push_back, emplace代替insert.

拷贝构造函数,赋值重载函数

参考链接
拷贝函数被调用的情况有:
1,定义新对象,并用已有对象初始化新对象时; 即执行语句“CExample B=A; ” 时(定义对象时使用赋值初始化);
2,当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数。
3. 对象作为函数参数返回

拷贝的目的:实现深拷贝原则上,应该为所有包含动态分配成员的类都提供拷贝构造函数

赋值重载目的:在深拷贝的同时,释放旧的内存,也用于动态分配成员的类。
“=” 表示赋值操作。将对象 A 的内容复制到对象C;,这其中涉及到对象C 原有内容的丢弃,新内容的复制。 但"="的缺省操作只是将成员变量的值相应复制。旧的值被自然丢弃。 由于对象内包含指针,将造成不良后果:指针的值被丢弃了,但指针指向的内容并未释放。指针的值被复制了,但指针所指内容并未复制。 因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载"="赋值操作符号。

拷贝构造函数 和 赋值运算符重载 为什么要使用引用

派生类对象的引用初始化基类引用。多态性的动态绑定中存在两个条件:1,必须是virtual 函数(虚函数);2, 必须是通过基类的引用或基类的指针进行成员函数的调用

malloc和c++内存管理

malloc结论:
1)当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)

2)当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

不错的链接

linux内存布局

在这里插入图片描述

在这里插入图片描述

上图就是linux操作系统的内存布局。内存从低到高分别展示了操作系统各个模块的内存分布。

代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

数据段:存放程序运行的时候就能确定的数据,可读可写。存储程序中已初始化的全局变量和静态变量

BBS段:定义而没有初始化的全局变量和静态变量,以及所有被初始化为0的全局变量和静态变量。

堆:堆的内存地址由低到高。调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

映射区:存储动态链接库以及调用mmap函数进行的文件映射

栈:栈。编译器自动分配和释放。内存地址由高到低。 使用栈空间存储函数的返回地址、参数、局部变量、返回值

内存分配原理

从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)

1)brk 是将数据段(.data)的最高地址指针 _edata 往高地址推

2)mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

当一个进程发生缺页中断的时候,进程会陷入核心态,执行以下操作:

1)检查要访问的虚拟地址是否合法

2)查找/分配一个物理页

3)填充物理页内容(读取磁盘,或者直接置0,或者什么都不做)

4)建立映射关系(虚拟地址到物理地址的映射关系)

5)重复执行发生缺页中断的那条指令

如果第3布,需要读取磁盘,那么这次缺页就是 majfit(major fault:大错误),否则就是 minflt(minor fault:小错误)

malloc具体分配

情况一:malloc 小于 128K 的内存,使用 brk 分配

将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:
在这里插入图片描述
1,进程启动的时候,其(虚拟)内存空间的初始布局如图1所示

2,进程调用A=malloc(30K)以后,内存空间如图2:

malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配
  事实是:_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。

情况二:malloc 大于 128K 的内存,使用 mmap 分配(munmap 释放)

在这里插入图片描述
默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存
这样子做主要是因为:

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。

在这里插入图片描述
 当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示

什么时候会发生段错误

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:

使用野指针
试图修改字符串常量的内容

死锁,未看完待续

死锁知识

字符串复制

参考链接

1char * strcpy(char* destination,const char * source);//遇到null字符结束

2char* strncpy(char* destination,const char* source,size_t num);//遇到空字符,n-num个空字符补全n

3void * memcpy(void* destination,const void* source,size_t num);//允许source的空字符复制

4void * memmove(void* destination,const void* source,size_t num);//允许重叠,比如例子:char str[] = "This is a test!";  memmove(str+2,str+10,4);

功能及用法说明:

1、strcpy:将由source指针指示的C 字符串(包括结尾字符)复制到destination指针指示的区域中。该函数不允许source和destination的区域有重叠,同时,为了避免溢出,destination区域应该至少和source区域一样大。

2、strncpy:复制source的前num字符到destination。**如果遇到null字符(’\0’),且还没有到num个字符时,就用(num - n)(n是遇到null字符前已经有的非null字符个数)个null字符附加到destination。**注意:并不是添加到destination的最后,而是紧跟着由source中复制而来的字符后面。下面举例说明:

char des[] = "Hello,i am!";

char source[] = "abc\0def";

strncpy(des,source,5);

//此时,des区域是这样的:a,b,c,\0,\0,i,空格,a,m,!  

//\0,\0并不是添加在!的后面。

这里,需要注意strcpy仅仅复制到null字符就结束了。

3、memcpy:将source区域的前num个字符复制到destination中。该函数不检查null字符(即将null字符当作普通字符处理),意味着将复制num个字符才结束。该函数不会额外地引入null字符,即如果num个字符中没有null字符,那么destination中相应字符序列中也没有null字符。同strcpy的区别:允许将source中null字符后面的字符也复制到destination中,而strcpy和strncpy则不可以。

4、memmove:同memcpy完成同样的功能,区别是,memmove允许destination和source的区域有重叠。而其他三个函数不允许。

例子:char str[] = “This is a test!”;

memmove(str+2,str+10,4);

静态成员有什么好处

参考链接

  • 静态成员函数主要为了调用方便,不需要生成对象就能调用(静态成员函数的作用基本上相当于:一个带有命名空间的全局函数。)
  • 基于跟类的实例无关,只跟类有关,不需要this指针的特性,他可以
    – 可以实现某些特殊的设计模式:如Singleton
    – 由于没有this指针,可以把某些系统API的回调函数以静态函数的形式封装到类的内部。
    – 可以封装某些算法,比如数学函数,如ln,sin,tan等等
    – 总之,从OOA/OOD的角度考虑,一切不需要实例化就可以有确定行为方式的函数都应该设计成静态的。

静态函数不需要实例化就可以被调用,不会也不可以调用或操纵非静态成员”

Singleton单例模式,未完待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值