C++后台开发知识点总结

继承多态封装的理解

  • 封装的意义在于保护或者防止代码(数据)被我们无意中破坏。

  • 保护成员属性,不让类以外的程序直接访问和修改,只能通过特定的方法进行访问。

  • 隐藏方法细节,只提供接口。

  • 继承主要是为了代码的复用,可以让派生类继承基类的对象、方法。并可以在现有基础上进行功能的扩展。

  1. 公有继承(public)

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

  1. 私有继承(private)

私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

  1. 保护继承(protected)

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
在这里插入图片描述

  • 多态主要是为了接口复用,具体是通过派生类重写基类的虚函数实现,通过将基类的指针指向相应的派生类对象来调用相应的方法,动态多态。

说一说内存泄漏

内存泄漏主要是由于疏忽导致未能正确释放掉程序中已经不使用的内存,造成内存浪费,系统运行速度慢,崩溃等后果。主要来源有两块:一是没能够正确使用free/delete释放malloc/new出的内存,导致堆内存泄漏。第二是系统资源泄露。主要指程序使用系统分配的资源比如SOCKET等没有使用相应的API释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

malloc与new

int *a = new int;
int *p = (int*) malloc(sizeof(int));
  • new和delete是C++关键字,需要编译器支持;malloc和free是库函数,需要头文件支持。
  • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
  • new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  • 对于自定义类型,new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)

指针和引用

  • 本质上,引用是别名,指针是变量指向一块地址。
  • 引用主要功能是传递函数参数和返回值
  • 指针在运行时可以改变所指向的值,而引用一旦与某个对象绑定后就不再改变。也就是指针可以被重新赋值以指向另一个对象,但是引用则总是在初始化时被指定的对象,以后不能改变,但是可以通过引用改变其绑定的内容。
  • 程序为指针变量分配区域,而不为引用分配内存区域。因为引用声明时必须初始化,从而指向一个已经存在的对象,引用不能为空值。
  • 指针便于使用字符串,动态分配内存

内存管理

栈:编译器自动管理、释放,主要存放局部变量,函数参数
堆:malloc/new free/delete 程序员自行分配的内存
代码区:存放函数体的二进制代码
字符常量区(只读数据区):存放常量字符串
全局或静态区:存放全局变量和静态变量;程序结束时由系统释放,分为全局初始化区和全局未初始化区

extern作用

  • 与“C”连用,如extern “C” void fun();告诉编译器按C的规则去翻译
  • 修饰声明全局变量或函数,其声明的变量和函数可以在其它模块(文件)中使用,注意,这只是一个声明而不是定义,具体的定义要在具体的模块中完成(头文件支持)

c++11新特性

1.auto关键字和decltype操作符,将类型推导工作交给编译器
2.基于范围的for循环,对于数组(或者容器类)中的元素进行循环。
3.lambda表达式
4.初始化参数列表
5.nullptr,传统为NULL,一个宏
5.5 int &&r_var2 = var + 40; // 正确:将右值引用 r_var2 绑定到求和结果上
6.新的智能指针,方便管理内存。本质上来说是将指针封装成对象,RAII。:

  • unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)

  • shared_ptr,共享指针应用于需要多个指针指向同一个对象的情况。
    int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
    我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

  • weak_ptr是为了配合shared_ptr而引入的一种智能指针,不增加引用计数,只是获得资源的观测权。
    在这里插入图片描述

说一说c++继承类型

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

c++ class和struct区别

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
  • class 可以使用模板,而 struct 不能。

智能指针

**智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。**C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
https://www.cnblogs.com/WindSun/p/11444429.html
c++11摒弃了auto_ptr
在这里插入图片描述
少了const,而每当auto_ptr被拷贝,它都会被置为null,相当于“移动”的语义。再次访问原ptr就可能会报错。
C++11引入了新的3种智能指针:
std::unique_ptr
std::shared_ptr
std::weak_ptr

  • unique_ptr:独占资源所有权的指针
    特点为:
    自动管理内存,离开智能指针作用域时自动释放内存
    只能被std::move()移交所有权,无法赋值或者拷贝构造
  • shared_ptr:共享资源所有权的指针
    特点为:
    对资源做引用计数——当引用计数为 0 的时候,自动释放资源
    实现中需要维护一个引用计数
  • weak_ptr:共享资源所有权的指针的观察者
    不影响shared_ptr的引用计数,当观察的shared_ptr管理的资源被释放时,自动变为nullptr,解决shared_ptr循环引用问题(在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。)

多态

  • 静态多态
    也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。实现方式为函数重载或者函数模板的使用。

  • 动态多态
    动态多态就是通过继承重写基类的虚函数实现的多态,因为实在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。

    在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

虚函数表、虚函数指针

一个类有一份虚函数表,一个类型有他自己的虚函数指针,指向虚函数表。每一个类都有自己独特的虚表。同时,在这个继承链上,编译器会为基类插入一个隐式的指针(一般是对象的首地址),指向虚表,称为__vptr。然后,子类继承父类时,会获得继承下来的__vptr,再根据自己的类的情况兼容(修改虚函数表里的值、发生偏移等。于是,当我们构建具体的类时,若是基类类型,__vptr就会指向父类的vtable,若是子类类型,__vptr就会指向子类的vtable。
在这里插入图片描述
ClassB继承与ClassA,其虚函数表是在ClassA虚函数表的基础上有所改动的,变化的仅仅是在子类中重写的虚函数。如果子类没有重写任何父类虚函数,那么子类的虚函数表和父类的虚函数表在内容上是一致的。

虚函数表 虚函数指针创建时期

- vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,也就是运行的时候。
当程序在编译期间,编译器会为构造函数中增加为vptr赋值的代码(这是编译器的行为),当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有为这个对象的vptr赋值的语句。

  • 虚函数表创建时机是在编译期间。编译期间编译器就为每个类确定好了对应的虚函数表里的内容。
    所以在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针,所以,这个虚函数表指针就有值了。

  • 所谓福类指针指向子类对象,本质就是指针指向一个地址,初始子类对象(new出一块新的内存地址)时会由于虚函数表机制,子类重写的父类虚函数在虚表中覆盖父类,因此调用对应虚函数时是调用的子类的,实现了多态。

c++内存对齐

位 :计算机内部数据储存的最小单位
字节:计算机中数据处理的基本单位, 1 个字节等于 8 位
在64位系统下:

  • int 4字节
  • short 2字节
  • char 1字节
  • float 4字节
  • long 8字节
  • long long 8字节
  • double 8字节
  • 汉字:GBK 2字节 UTF-8 3字节

CPU从内存中获取数据时起始地址必须是地址总线宽度的倍数,内存对齐的目的是让cpu能够更加高效便捷取得数据。

内存对齐三原则:

  • 内置类型数据成员:结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身大小的整数倍开始存储
  • 结构体作为成员: 如果一个结构里有某些结构体成员,则结构体成员要从其内部“最宽基本类型成员”的整数倍地址开始存储(如struct a里存有struct b,b里有char, int, double等元素,那b应该从8的整数倍位置开始存储)。
  • 收尾工作: 结构体的总大小,也就是sizeof的结果必须要对齐到内部"最宽基本类型成员"的整数倍,不足的要补齐。(基本类型不包括struct/class/union)。
class Data
{
    char c;
    int a;
    char d;
};
 
cout << sizeof(Data) << endl;

class Data
{
    char c;
    char d;
    int a;
};
 
cout << sizeof(Data) << endl;

(3)+1+4+(3)+1 = 12
1+1+(2)+4=8

 class A {                                                                                         
 public:                                                                                           
     double len;                                                                                   
     char str[33];                                                                                 
 };                                                                                                
                                                                                                   
 class B {                                                                                         
 public:                                                                                                                                          
     A a;                                                                                          
     int b;                                                                                        
 };

cout << sizeof(A) << "  " << sizeof(B) << endl;

8+33+(7)=48
48+8=56

内存对齐原因:

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。
    在程序员看来,内存是由一个个的字节组成。而CPU并不是这么看待的,CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度) 本人把它翻译为“内存读取粒度” 。

构造函数、赋值函数、拷贝构造函数

  • 拷贝构造函数使用时机
    (1)一个对象以值传递的方式传入函数体
    (2)一个对象以值传递的方式从函数返回
    (3)一个对象需要通过另外一个对象进行初始化。

  • 对象不存在,且没用别的对象来初始化,调用构造函数
    A a;

  • 对象不存在,且用别的对象来初始化,调用拷贝构造
    A a;
    A b = a; 或 A b(a);

  • 对象存在,用别的对象给他赋值,就是赋值函数
    A a;
    A b;
    a = b;
    补充知识点:构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。

关于explict

class A
{
public:
	explicit A(int n)
		: _n(n)
	{}
	A(const A& a)
		: _n(a._n)
	{}	
private:
	int _n;
};

int main()
{
	A a1(1);//他的生命周期在main函数域
	A(5);//匿名对象,他的生命周期在这一行
	
	A a2 = 5;
	//隐式类型转换,结果调用构造函数 -> 先构造一个A(5)匿名临时对象,
	//再用临时对象拷贝构造a2
	return 0;
}

用explicit修饰构造函数,将会禁止单参构造函数的隐式转换,相当于禁止了拷贝构造。下面的例子可以看下,如果不用explict会有bug
另外只有参数位于参数列(paramaeter list)内才会发生隐式转换。

#include <iostream>
using namespace std;
#include<algorithm>
class Str{
	public:
	 Str(int x){
		cout<<"我是想把整数变字符串"<<endl;
	}
	Str(const char* a)
	{
		cout<<"我是想把字符数组变字符串"<<endl;
	}

};


int main( ){

	Str s='c'; 
	// 输出:"我是想把整数变字符串"
	// 它把'c'的ASCII码传进去了,如果这样变成字符串那就得到一个数字,
        // 而我们期待的是把'c'变成字符串。
	return 0;
}

常量和引用必须在声明的时候初始化

匿名对象

class Cat
{
public:
    Cat()
    {
        cout<<"Cat类 无参构造函数"<<endl;
    }
    Cat(Cat& obj)
    {
        cout<<"Cat类 拷贝构造函数"<<endl;
    }
    ~Cat()
    {
        cout<<"Cat类 析构函数 "<<endl;
    }
};
void playStage() //一个舞台,展示对象的生命周期
{
    Cat();             /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;执行完此行代码,因为外部没有接此匿名对象的变量,此匿名又被析构了*/
    Cat cc = Cat();    /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;然后将此匿名变成了cc这个实例对象,此匿名对象没有被析构。*/
    cout<<"cc 对象好没有被析构"<<endl;    
}
int main()
{
    playStage();
    system("pause");
    return 0;
}


输出:
Cat类 无参构造函数
Cat类 析构函数
Cat类 无参构造函数
cc 对象好没有被析构
Cat类 析构函数

动态库与静态库的区别

在这里插入图片描述
在这里插入图片描述
动态库文件(.dll)静态库文件(.lib)都是由 多个.o文件打包生成,但链接静态库较为占用空间,且静态库更新时容易造成整个目标文件都得重新编译。动态链接的过程只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息,只是执行的时候,动态库的内容被映射到响应进程的虚拟空间。
静态库被打包到最终的应用程序中,加载速度快,发布的时候无需提供静态库,移植方便(直接给可直行程序就可以)
动态库使用的时候才加载,动态链接的过程只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息,更新版本迭代的时候比较方便。

const和static

关于static可从两方面回答:一是和类的成员函数或者成员变量相关,二是不属于类的函数或者变量。

  • static:主要作用是改变生命周期或者改变作用域。
    当作用于非类的变量时:
    (1)用static声明的局部变量,延长生命周期(储存在静态区,编译期间分配内存),但是作用域仍为函数体内(仍为局部变量)
    (2)用static声明的外部变量,改变连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。const修饰全局变量也是变成local const,只在本文件内有用,其他文件extern 声明也不行。

当作用于类的变量时:
对于类的变量,static声明为静态变量,静态变量类的内部定义,外部初始化,属于整个类,在外部访问可直接用类名+访问限定符。
对于类的函数,static静态为静态成员函数,无this指针,只能访问类的静态成员变量。

  • 关于const
    const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。
  • 修饰基本数据类型
    需要在声明时就初始化,因为一旦声明后,无法修改值。
    修饰指针时:
const int* a = &b;
int* const a = &b;

离哪个近,就说明哪个不可修改。也可以同时修饰之指针和指针所指向的内容。

指针的类型必须与其所指对象的类型一致,但是有个例外。允许为一个常量引用(或指针)绑定非常量的对象、字面值或者表达式。
也就是说:

int i = 42;
const int &r1 = i;

const int i = 42;
const int &r1 = i;

都可行的


//常量指针,指针作为对象它本身(所指的对象)以及pi存的那个地址都不能改变
const double *const pip = &pi;
  • 应用到函数中,修饰参数或者返回值
  • 修饰成员函数,防止成员函数修改被调用对象的值
void fun1(const A& a);

无法对传进来的引用对象进行改变。
若修饰函数,使其返回值无法成为左值。

  • 非静态成员函数后面加const(加到非成员函数或静态成员后面会产生编译错误),表示成员函数隐含传入的this指针为 const指针,决定了在该成员函数中,任意修改它所在的类的成员的操作都是不允许的(因为隐含了对this指针的const引用)

  • 类中定义常量

class CTest11
{
public:
    static const int a = 3; // Ok in C++11,类中常量,每个类有一份
    static int b = 4;       // Error,静态成员变量需要类外初始化
    const int c = 5;        // Ok in C++11
    int d = 6;              // Ok in C++11
public:
    CTest11() :c(0) { }     // Ok in C++11
};
 
int main()
{
    CTest11 testObj;
    cout << testObj.a << testObj.b << testObj.c << testObj.d << endl;
    return 0;
}

只能使用初始化成员列表的几种情况:

  • 初始化const成员
  • 初始化引用成员
  • 需要初始化的数据成员是对象只有含参数的构造函数,没有无参数的构造函数.

迭代器失效

迭代器失效就是因为插入和删除,使得原本通过迭代器可以访问到容器内的元素,变得无法再访问。因为插入和删除可能更改了元素在内存中的位置,原来迭代器指向的位置不再存储原有的值。

  • 数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(iter)(或erase(iter)),然后在iter++,是没有意义的。解决方法:erase(iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

  • 链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(iter)会返回下一个有效迭代器的值,或者erase(iter++).

  • 树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

c++类型安全

类型安全是指同一段内存在不同的地方,会被强制要求使用相同的办法来解释(内存中的数据是用类型来解释的)。其实就是内存安全

int main()
{
	printf("%f\n",10);
	system("pause");
	return 0;
}

c++ 数组 vector 链表

  • vector和数组类似,内存空间连续,所以存取高效,O(1)。也是因为连续,所以插入和删除会造成内存的拷贝,复杂度O(n)
  • 链表内存不连续,插入O(1),存取O(n)
  • 定义数组的时,必须指定数组的类型和大小

类型转换

C++ vector的底层


#ifndef MYVECTOR_H
#define MYVECTOR_H
class MyVector
{
private:
    int *data;    //实际存储数据的数组
    int capacity; //容量
    int _size;
    void resize(int st)
    {
        //重新分配空间,在堆区新开辟内存,然后将以前数组的值赋给他,删除以前的数组
        int *newData = new int[st];
        for (int i = 0; i < _size; i++)
        {
            newData[i] = data[i];
        }
        delete[] data;
        data = newData;
        capacity = st;
    }
 
public:
    // 构造函数
    MyVector()
    {
        data = new int[10];
        capacity = 10;
        _size = 0;
    }
    ~MyVector()
    {
        delete[] data;
    }
    MyVector(int st)
    {
        data = new int[st * 2]; //初始化的时候 容量分配为2倍size,实际vector不知是不是这样做的
        capacity = st * 2;
        _size = st;
    }
    void push_back(int e)
    {
        //如果 当前容量已经不够了, 重新分配内存, 均摊复杂度O(1)
        if (_size == capacity)
        {
            resize(2 * capacity);
        }
        data[_size++] = e;
    }
    int pop_back()
    {
        int temp = data[_size];
        _size--;
        //如果 容量有多余的,释放掉
        if (_size == capacity / 4)
        {
            resize(capacity / 2);
        }
        return temp;
    }
    int size()
    {
        return _size;
    }
    //重载[]操作
    int &operator[](int i)
    {
        return data[i];
    }
};
#endif

数组实现,如果内存不够了,就重新申请一块内存,扩容为之前2倍,然后把之前的内容都复制进去。

volitale关键字

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

#include <stdio.h>
 
void main()
{
    int i = 10;
    int a = i;
 
    printf("i = %d", a);
 
    // 下面汇编语句的作用就是改变内存中 i 的值
    // 但是又不让编译器知道
    __asm {
        mov dword ptr [ebp-4], 20h
    }
 
    int b = i;
    printf("i = %d", b);
}

debug模式运行:
i = 10
i = 32

release模式运行:
i=10;
i=10;
Release 模式下,虽然i变了,编译器对代码进行了优化,直接从寄存器读取上一次存的i的值,而不是从内存读取值。

左值 右值引用

  • 右值引用作用1:移动语义
    通过利用右值临时存在的特性,避免自定义类中深拷贝或者重载赋值操作符造成的对临时对象的多次构造、析构浪费资源,以提高效率。看例子:
class Stack
{
public:
    // 构造
    Stack(int size = 1000) 
	:msize(size), mtop(0)
    {
	cout << "Stack(int)" << endl;
	mpstack = new int[size];
    }
	
    // 析构
    ~Stack()
    {
	cout << "~Stack()" << endl;
	delete[]mpstack;
	mpstack = nullptr;
    }
	
    // 拷贝构造
    Stack(const Stack &src)
	:msize(src.msize), mtop(src.mtop)
    {
	cout << "Stack(const Stack&)" << endl;
	mpstack = new int[src.msize];
	for (int i = 0; i < mtop; ++i) {
	    mpstack[i] = src.mpstack[i];
	}
    }
	
    // 赋值重载
    Stack& operator=(const Stack &src)
    {
	cout << "operator=" << endl;
	if (this == &src)
     	    return *this;

	delete[]mpstack;

	msize = src.msize;
	mtop = src.mtop;
	mpstack = new int[src.msize];
	for (int i = 0; i < mtop; ++i) {
	    mpstack[i] = src.mpstack[i];
	}
	return *this;
    }

    int getSize() 
    {
	return msize;
    }
private:
    int *mpstack;
    int mtop;
    int msize;
};

Stack GetStack(Stack &stack)
{
    Stack tmp(stack.getSize());
    return tmp;
}

int main()
{
    Stack s;
    s = GetStack(s);
    return 0;
}

运行结果

Stack(int)             // 构造s
Stack(int)             // 构造tmp
Stack(const Stack&)    // tmp拷贝构造main函数栈帧上的临时对象(编译器不优化的情况下)
~Stack()               // tmp析构
operator=              // 临时对象赋值给s
~Stack()               // 临时对象析构
~Stack()               // s析构

自定义的拷贝构造函数和赋值运算符重载函数通过开辟较大的空间,然后将数据逐个复制)解决浅拷贝,但其中tmp和临时对象都在各自的操作结束后便销毁了,使得程序效率非常低下。

// 带右值引用参数的拷贝构造函数
Stack(Stack &&src)
    :msize(src.msize), mtop(src.mtop)
{
    cout << "Stack(Stack&&)" << endl;

    /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
    mpstack = src.mpstack;  
    src.mpstack = nullptr;
}

// 带右值引用参数的赋值运算符重载函数
Stack& operator=(Stack &&src)
{
    cout << "operator=(Stack&&)" << endl;

    if(this == &src)
        return *this;
	    
    delete[]mpstack;

    msize = src.msize;
    mtop = src.mtop;

    /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
    mpstack = src.mpstack;
    src.mpstack = nullptr;

    return *this;
}

为了提高效率,把tmp持有的内存资源直接给临时对象,把临时对象的资源直接给s。

类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

  • 右值引用作用2:完美转发

C++和PHP的区别

  • 应用场景

在谈两种语言不同的时候,首先需要了解两中语言的主要语言场景。C++与PHP的应用场景有比较大区别,C++作为一门拥有悠久历史的语言,已经应用的场景非常广泛,已经有数不清的项目使用C++。由于阅历有限,目前想起来的C++主要的应用场景有:对性能有要求的服务器,游戏的渲染引擎,一些和硬件打交道的场景(例如和一些电表通信)。而PHP主要是用在服务器脚本程序,用于web。

  • 编译器的解释方式

C++是一门静态语言,需要先进行编译,产出程序才能执行,C++的类型是固定的。而PHP则不同,是一门动态语言,边解释变运行,同时PHP也是弱类型语言,即“变量的类型通常不是由程序员设定的,确切地说,是由 PHP 根据该变量使用的上下文在运行时决定的”。

  • 数组的处理

对于C++来说,数组是一个危险的存在(这点和c一样),因为没有越界检查,在声明的时候需要确定数组的类型和长度(这样编译器就确定为其分配多少内存)。而对于PHP来说,数组更像C++中的map,写入的时候若没有对应的键值,会添加此键值,但是若读取的时候没有此键值的话,就会报出错误。

  • 赋值

对于C++和PHP来说,变量1=变量2是相同的,都是为变量1重新分配了内存。而对象之间的赋值就不同了,C++两个对象的赋值直接使用”=“,即”operator=()“,例如obj1 = obj2,是将obj2的内容全部拷贝到obj1里面,而PHP直接使用”=“类似于C++的引用赋值,obj1并没有分配内存,只是指向obj2的内容。php浅拷贝clone,深拷贝_clone()函数

  • 在面向对象方面
    多态
    因为PHP是弱类型语言,所以他的多态性到处都有体现,导致他的多态性不像C++中那么明显。比如,在PHP中基类的函数可以看作全是virtual的,因此它不需要加任何修饰符,子类中和基类同名的函数都会被动态调用,而C++不一样,如果基类中的这个函数没有加virtual修饰符,子类中的那个同名函数就不会被动态调用,只能静态调用了。

  • 操作符重载
    PHP中不存在,而C++存在。重点在操作符,在PHP中他可以运用在任何类型上,即使这个类型没有(像C++中一样)写自己的重载函数。PHP中对于对象的比较,表示两个对象的属性和值都一样,而且类型也一样;PHP也存在一个=操作符,内容类型都相同。

  • 构造函数
    C++中子类的构造函数默认会调用父类的构造函数,而PHP中不会;C++中你必须有一个默认的构造函数,这个构造函数必须在没有参数的时候也可以执行,而PHP中没有这个要求,你甚至可以不需要构造函数,如果你的某个函数没有使用类中的对象,你甚至可以将它按static 类型函数来使用。

  • 继承
    PHP不支持多继承,继承用extern关键字。

类型转换

https://www.cnblogs.com/evenleee/p/10382335.html

std::move()

  • 它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
  • C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
  • std::move是为性能而生。
  • std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。//这里说的不是很准确,9.15更新

std::move() 并不会真正地移动对象,真正的移动操作是在移动构造函数、移动赋值函数等完成的
std::move()底层大概是这个样子:

template<typename T>
typename remove_reference<T>::type&& move(T&& value){
    return static_cast<typename remove_reference<T>::type&&>(value);
}

c++11之后,operater=(赋值运算重载函数)有两个重载,一个复制,一个移动

string& operator=(const string& another); // 赋值
string& operator=(string&& another); // 移动

诸如string b = a, 如果a是move后的右值引用类型,编译器调用移动赋值函数。同时在移动构造函数或者移动赋值函数里进行了真正的移动操作,一般将原有右值置为空。举个例子

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){//效率比移动赋值低
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){//直接赋值
        d.num = NULL;//原指针置空
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

构造函数调用虚函数

#include <iostream>
using namespace std;
class A {
public:
	A() {
		this->fun();
	}
	virtual void fun() {
		cout << "A fun" << endl;
	}
};
 
class B :public A {
public:
	B() {
		this->fun();
	}
	virtual void fun() {
		cout << "B fun" << endl;
	}
};
 
int main() {
	B b;
 
	return 0;
}

在这里插入图片描述
原因也很简单,仔细过一下构造函数的执行流程即可。调用B的构造函数,先调用A的构造函数,调用A的构造函数,先按照A对象的内存布局进行初始化,因为虚表指针是放在顶部的,先初始化虚表指针,指向虚表(虚表在编译期就生成),之后按照声明顺序初始化成员变量。最后调用构造函数{ }中的代码。由此可见调用this->fun( )时已经设定好虚表指针,所以调用不会有任何问题。之后B将虚表指针指向自己的虚表,初始化自己的成员变量,最后调用B(){ }中的代码this->fun( ),这个时候对象顶部的虚表指针指向B的虚表,调到的自然是B::fun( )。先构造父类在构造子类。

构造和析构函数本身能是虚函数

构造函数不行。因为虚函数指针指随着对象创建而创建,指向虚函数表是在运行时期动态绑定的,对象没构建则没有虚函数指针,没法调用虚函数。
析构函数得是虚函数,因为要保证子类析构->基类析构,否则会内存泄漏。

类对象构造如何只在堆/栈

class A  
{
 
protected:  
    A(){}
    ~A(){}
    
public:
    static A* create()// 如果在C++类声明了static数据成员和static函数,一定要在类的声明体外面定义static数据成员。可以在类的声明体内定义或者在声明体外定义static函数。在定义时,一定要将关键字"static"去掉,否则会有编译错误
    {
        return new A();
    }
    
    void destory()
    {  
        delete this;
    }
};

将构造和析构都生命为protected对象,可以:(1)类外无法直接调用构造函数创建对象(2)protected对象仍可被子类继承,可实现多态。
提供公用方法在堆上构造与析构,类似单例模式。

  • 将operator new()设为私有(new底层)即可禁止对象被new在堆上
class A
{
private:
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void* ptr){}  // 重载了new就需要重载delete
    
public:  
    A(){}
    ~A(){}
};

全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值