欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++知识点全面汇总

程序员文章站 2022-07-12 15:41:29
...

不定期更新C++14/17/20的新玩意

C++编译流程

预处理-》编译-》汇编-》链接

cpp-》预处理器-》编译器-》汇编程序-》目标程序-》链接器-》可执行程序

C++ 基础语法知识

向下兼容知识

各内置类型大小

64位下:

char 1 char* 8 short int 2 int 4 unsigned int 4 float 4 double 8 long 8

32位下:

与64位的区别在于char* 4

数组与指针的区别

数组名是一个指向一个连续已分配地址的指针,并且是一个常指针变量,无法赋值,无法自增自减操作,在函数传参的过程中会退化为一般指针。

char* tp=new char('1');
char str[10];
str++;//error
str=tp;//error

extra: strlen()计算字符串的长度,以’/0’为结尾,不包括/0

C风格字符串以及相关操作

使用char数组,注意最后一个字符是’/0’,因此n个大小的数组能够存储n-1个值。声明字符串如下

char strs[6]="hello";
char strs2[]="hello";

如果用sizeof计算会得到6,而用strlen计算会得到5

常用字符串操作:

strcpy(s1,s2);//将s2复制到s1
strcat(s1,s2);//将s1与s2连接
strcmp(s1,s2)//返回字典序的s1-s2,相等为0

**细节:**无论C/C++中,显示给出的字符串值都是const char*类型的

extern

表明变量或者函数定义在其他文件中,在其他文件中寻找这个变量(这两个文件必须链接起来)

在a.cpp中定义一个函数或者变量,

b.cpp中声明extern变量和函数,表明要使用的量定义在其他文件中,并通过将链接来使用。

注意

  1. a中变量或者函数不能是static 否则会被隐蔽掉
  2. 必须链接起来

C风格的输入输出

static

有以下个作用

  1. static用于修饰静态局部变量:用static修饰的变量会存在在虚拟内存的全局数据区域,只初始化一次(首次声明时),全局数据区域内的数据离开作用域不会被销毁,它的生命周期和程序运行周期相关。

    void add()
    {
    	static int b=0;
    	b++;
    	return b;
    }//运行三次得到1,2,3,因为该区域的变量没有被销毁
    

    定义静态局部变量的好处是,变量属于函数,并且可以保留上次调用的值,并且不污染名称空间。但是作用域还是局部作用域

  2. 静态全局变量

    存储在全局数据区的量,但该变量只在本文件可见,其他文件无法通过extern来使用该变量,从而避免重命名问题

  3. 静态全局函数

    存储在全局数据区的量,但该函数只在本文件可见,其他文件无法通过extern来使用该函数,从而避免重命名问题

  4. 静态成员变量与成员函数详见OOP部分

结构体struct

C++中的结构体,可以拥有成员函数,可以拥有静态成员,可以有访问控制权限,可以有继承关系,可以初始化数据成员。区别在于:

默认访问权限是public

C++中有一个约定:struct往往用来定义数据结构,所操作数据

头文件

include"name"是在本地工作目录中寻找

include是在标准库中寻找

C++高级语法速览

OOP以下

宏定义

C++预处理命令之一,它是一个替换操作,不做计算和表达式求解,不占内存和编译时间

inline

向编译器请求为内联函数(编译器有权拒绝),可以在运行时调试,如果在类中定义成员函数会自动转换成内联函数,可以定义在头文件中。

与普通的函数区别在于,普通函数调用时需要记下返回地址,保存现场,执行跳转语句,再调用,而内联函数是通过函数体代码和实参直接代替函数调用语句,性能会得到提高很多。

虚函数,递归,迭代,内联函数内调用其他函数,一般不会内联。

auto

自动推断类型 类似C# var

可用于默认类型,自定义类型,可调用对象(函数,Lambda等)

不能使用引用, 会丢弃const属性,不能用于函数参数与普通成员变量

for_each

本质上是一个函数模板,其模板的实现大致如下

Functino for_each(Iterator beg,Iterator end,Function f){
    while(beg!=end)
        f(*beg++);
}
//for_each(起点,终点,对每一项进行处理的函数)

一个用for_each遍历vector的例子

for_each(nums,begin(),nums.end(),[](int n){cout<<n<<" "});

decltype

可以得到表达式的类型,但并不进行计算,一共有如下这些使用方法

const int a=0;
int& b=a;
int* c=&a;

decltype(a);//得到const int
decltype(b);//int&
decltype((a));//int& 双括号得到引用
decltype(b+0);//得到int
decltype(c);//得到int*
decltype(*c)//得到int& c原本是指针,取指针最终得到的是所指对象 相当于引用

const

常用用法

class MyClass{
public:
  void fun()const;
    //其中的this指针指向const成员变量, 等价于
    //const MyClass* const this; 也就是说无法改变类中除了mutable外的属性
};
const int a=0;//常量,必须初始化
const MyClass a1;//常量类对象,只能调用const成员函数
const int* a2;//指针本身可以变,但是其内容不能变
int* const a3;//指针本身不能变,但是其内容可以变
const int* const a4;//指针指向不能变,内容也不能变
const var& function()//不能使用返回值的引用来修改对象

const定义的常量会进行类型检查,而define定义的常量不会

const还可以用来重载成员函数const void func() const;使得const对象也能调用对应函数

别名定义

typedef using

两个关键字都用来进行别名定义,可以为默认类型,对象,结构体,数组,函数进行别名定义

//typedef用法
typedef int Array[10000];
Array a;//直接得到了一个数组
typedef struct {double x,double y} vector;//定义结构体
typedef int(*F)(int,int),int* INT;//typedef可以用逗号分割去定义多个别名

//using用法
using namespace std;//引用名称空间,注意头文件往往不会使用using
using INT = int*;//将右边的类型定义为左边的别名
using INT_Vector=int[50];//using是不能多个联用的
using Fa=int (*)(int,int);//注意两者定义委托的方式区别较大

指针与引用

指针和引用都间接操作对象

区别

指针是一个实体,但是引用是别名

指针可以多级指向,但是引用绑定了最初获得的对象后就无法变更,因此只能引用一级(哪怕这样在语法上是不会报错的)。

指针可以为空,引用不行

指针可以为const,引用不行

指针使用前可以不初始化,引用一定要初始化

sizeof运算结果不同,sizeof指针得到指针大小,sizeof引用得到所指对象大小

int a=5;
int b=6;
int& n1=a;
int& n2=b;
n1=n2;
n1=1;

上述代码虽然n1=n2,但n1仍然指向a,修改值后,a的值为1,b不变。

智能指针

unique_ptr,shared_ptr,weak_ptr

shared_ptr

通过引用计数来实现的一种指针,如果当前指针其引用数为0,那么就会清空该指针所指向内存,常用方法如下:

shared_ptr<MyClass> p0;//不初始化的话为nullptr空指针
shared_ptr<int> p1(10);//默认类型直接使用数值初始化
shared_ptr<int> p2(new int(10));//可以使用裸指针初始化,不推荐
shared_ptr<MyClass> p3=make_shared<MyClass>(10);
//用make_shared指定类型初始化,只要有满足所传参数的构造函数即可

*p1;//解指针
p1.get();//返回裸指针,如果清空这个裸指针会使得智能指针无法访问所指内存
p2=p1;//p1引用计数++,p2的--
p1.user_count();//返回引用计数

函数传值,返回值(非引用),将赋值给一个新的智能指针都会增加引用计数

离开作用域,被赋予了一个新的智能指针都会减少引用计数

一般来说智能指针会再清空时自定义调用delete,但是有些情况下需要自定义deleter,比如通过shared_ptr管理数组(一般不建议这么搞) shared_ptr没有自定义[]操作,需要通过get得到指针后显示访问数组。

shared_ptr<int> nums(new int[10],[](int *p){delete[] p;})

其他常用方法

nums.reset();
//根据当前引用计数情况
//1. 如果当前引用计数为1,那么就会调用删除其
//2. 否则直接置空当前指针

unique_ptr

不支持拷贝,不支持赋值,必须直接用裸指针初始化,与共享的shared_ptr不同,某一时刻只能有一个unique_ptr指向一个对象。

初始化方式

unique_ptr<int> ptr;
unique_ptr<int> p0(new int(10));

通过release可以让当前unique_str指针放弃所指向的内存,并返回其所指内存的指针(),通过这个函数可以转交内存权限,release返回的是裸指针

unique_ptr<int> ptr;
unique_ptr<int> p0(new int(10));
ptr.reset(p0.release());//调用完release后p0就视为了nullptr
unique_ptr<int> 

自定义删除器

weak_ptr

辅助shared_ptr,指向其所指的对象,但是不控制其对象的生命周期(不会修改引用计数),也不会影响其释放对象。

动态分配数组

type* name=new type[const size];

delete[] name;

上述方法动态分配返回得到的是一个指针而不是一个数组类型

如果分配的是指针数组,在清空时要遍历整个数组清空每个指针后,再清空数组指针。

int** a=new int*[10];
for(int i=0;i<10;i++)
	delete[] a[i];
delete[] a;

智能指针分配数组

shared_ptr<int> nums(new int[10],[](int* p){delete[] p});//shared_ptr要自定义删除器,并通过get得到裸指针偏移来访问元素
unique_ptr<int[]> nums(new int[10]);//通过元素访问运算符即可访问元素

右值引用

左值往往是持久化的变量,右值往往是临时(即将要销毁)的数据

右值引用指的是把变量绑定到右值

int&& i = 42;//右值引用
int& j=i;//左值引用
int&& k=i;//这条语句会报错,int&& 得到的仍然是变量视为左值

往往可以用右值引用来接函数返回值来略微提高性能

Move

对变量进行移动,头文件utility

可以把一个左值转换成右值后,就无法再使用该左值,视其是无效的(可以新赋值或者销毁)

int a=5;
int&& b=move(a);//左值该成右值,现在a,b使用同一块内存空间
a=10;//修改a=10后,a,b均为10,因此绝对不能修改被移动的遍历,应该视作其是无效的

移动与拷贝的区别:

拷贝对象相当于新开辟一块内存并把拷贝的数据复制过来

移动对象则是指新创建的对象会使用被移动内存空间,省下了内存开辟和收放的开销。

可调用运算符()

如果在类/结构体中重载该运算符,则会将类对象变成可调用类型,如下

struct cmp{ bool operator()(int a,int b){return a>b; }//仿函数
cmp()(a,b);//在生成对象后,可以直接调用这个对象

对于重载了可调用运算符的类/结构体,其生成的对象无法赋值给函数指针,但可以给function对象

C++函数多态

  1. 函数指针

    type (*function)(…type);声明一个函数指针量(不是类型),且只能接受函数(有些可调用对象比如重载了()运算符的类对象接受不了)

    bool (*Compare)(int,int);
    Compare=cmp;//可以声明函数指针变量(类似委托),将不同的函数赋值给它
    //注意如下的代码是错误的
    /*
    Compare c=cmp;函数指针是一个量,不是类型,如果要定义成类型可以使用using或者typedef
    */
    
  2. function模板

    需要头文件#include相当于把函数指针声明类型的两步变成一步

    function<return_type(…pars_type)> func;

    function<bool(int,int)> Cmp=cmp();
    

    function模板相当于C#中的委托,其可以接受函数,函数指针,可调用对象,Lambda表达式

Lambda

可调用对象,匿名函数

//基本格式:[捕获列表](参数列表)->返回值类型{函数体}
//除了捕获列表与函数体其他均可以为省略(捕获列表可以为空)
//捕获列表是指所使用的局部变量,其他顾名思义
int a=1,b=2;
[a,b]{return a>b;}

//如果一个lambda表达式包含return之外的语句在未定义返回类型的情况下,其返回值都为void
//return a>b; if(a>b) return true; else return false; 两者返回可能是不同的
[a,b]->bool{if(a>b)return true;else return false;}

//捕获列表中的局部变量也可以是引用或者指针
[&a]{a++;}
[&]{}//引用外部区域的所有变量
[=]{}//按值使用外部区域所有变量
[this]{}//lambda表达式拥有成员函数的权限

//指定参数
[](int a,int b)->bool{return a>b}//接受参数的匿名函数,这个例子中的返回值类型可以省略

bind关键字

头文件functional,placeholders,可以将固定参数传递给一个可调用对象并返回一个新的可调用对象

//bind(callable,...pars)
//其传递的参数需要严格与callable对应,一共有两类参数 _i(i=1,2,3..n)占位符与实际参数
//_i占位符相当于暂时占据了callable中参数的位置,新调用对象需要将参数传递给这些形参
//实际参数相当于已经赋值给了新调用对象

auto newcallable=bind([](string s,int n){return s.size()>=n;},_1,2);
newcallable("str2");//返回true
//上述的这个例子,bind将数值2绑定到了匿名函数的形参n,接下来的新调用对象只需传递实参给形参s即可。

auto newcallable=bind([](int a,int b,int c,int d){},_1,2,_2,2);
newcallable(2,2);
//上述的例子 根据占位符所占的位置是匿名函数的第1,3的位置,而实参已经传递给了其第2,4个位置,因此新调用对象使用时要传递第1,3个参数

异常处理

noexcept

OOP以上

OOP三原则

封装:使数据和加工该数据的方法封装成整体增加安全性

多态:同一个类型可以根据不同的消息做出不同的事件

继承:子类共享父类数据和方法的机制,增强复用性。

成员

数据成员必须是完全类型(变量等),指针成员,静态成员可以是不完全类型

class ListNode
{
public:
	ListNode* next;//由于此时ListNode的成员尚未初始化,所以是不完全类型
	//ListNode next;因此这条语句是会报错的
}

构造函数

默认构造函数,在未定义构造函数情况下编译器会提供一个默认构造函数,一个函数如果定义了其他函数,编译器就不会自动提供,需要自行定义默认构造函数,尤其是当类中含有其他类型信息,指针,数组时

class MyClass{
public:
    MyClass()=default;//定义为默认构造函数
}
MyClass myc;

对于参数列表全部提供了默认值的构造函数也会视作默认构造函数

=delete可以把函数定义成删除的,也就是无法调用的,如果将拷贝控制函数(构造,复制,赋值,析构等)定义成删除的,会导致对象无法进行相应的操作。如果类中有成员是类类型的,其中存在=delete的拷贝控制,也会对当前类的对应功能造成=delete。

不能有多个默认构造函数,会有二义性

以下情况必须提供默认构造函数

  1. 声明类型数组,且未初始化
  2. 声明变量,且未指定初始化值
  3. 作为其他类的成员变量时

构造函数初始化列表

使用初始化列表的构造函数直接初始化对象,而通过赋值进行初始化的构造函数,会先对成员进行默认初始化后再进行赋值,这对于有些成员比如const或者引用是不能接受的。

拷贝构造函数

编译器会为我们提供默认的拷贝构造函数,逐元素进行拷贝操作(如果类类型,会用类类型的拷贝构造函数)

MyClass(const MyClass& ms)//形式化描述
{
    val=ms.val;
}

以下情况会使用拷贝构造函数:

传对象给非引用形参

返回对象为非引用类型

用花括号来初始化类型数组

拷贝初始化

用已给对象拷贝来进行对象初始化的本质是调用将已给对象传入拷贝构造函数创建了一个新对象

拷贝赋值运算符

编译器也会自动提供该成员

MyClass& operator=(const MyClass& ms)
{
    this->val=ms.val;
    return *this;
}

返回当前对象的引用

析构函数

一个对象离开作用域,delete,容器被清空时等会调用析构函数,编译器会自行提供

~MyClass(){}

析构函数并不销毁成员,销毁成员时析构完毕后的析构阶段做的,析构函数的调用顺序与构造相反,是逆序调用的(后构造的先析构

自定义的析构往往会挂钩拷贝构造函数,拷贝赋值运算符重载

移动构造函数

移动构造函数使得对象创建时,会使用右值引用参数对应成员的空间,而不是像拷贝构造函数重新开辟空间后初始化。(内存处理是转移而不是拷贝

class MyClass{
public:
    int *val;
	MyClass(const MyClass&& t)
    {
        if(this != &t)//先确保不是自赋值
        {
            if(t->val!=nullptr)
            {
                val=t->val;//使得所指空间相同
                t->val=nullptr;//使用移动构造函数需要使形参中指针为空,防止销毁时清空了调用对象的内存
            }   
        }
    }
}

移动赋值运算符

类似做法

MyClass& operator=(const MyClass&& mc)

返回新建立对象的引用

两个移动函数不一定会由编译器自行创建

混合构造函数使用的例子

class vector2{
public:
	double x;
	double y;
    vector2(double X=0,double Y=0):x(X),y(Y=0);
    vector2(const vector2 v):x(v.x),y(v.y){}
    vector2& operator=(const vector2 v)
    {
        x=v.x;y=v.y;
        return *this;
    }
}
vector2 vcts[2];//首先调用2次默认构造函数,完成初始化
for(int i=0;j<2;i++)
    vcts[i]=vector2(i,i);//每次调用一次默认构造函数,赋值运算符函数,以及析构函数
//最后运行结束再调用两次析构函数

深浅复制问题

浅拷贝往往只拷贝了对象 而不是拷贝对象中的内容 这会导致如果销毁拷贝对象可能会影响源对象中的值

深拷贝是拷贝了对象的内容,使得拷贝对象与被拷贝对象间完全独立。

友元

在类中定义的非类成员函数,使得其获得与类成员函数一样的权限 关键字friend

class MyClass(
    friend istream& read(istream& is,MyClass m);
    private:
    	string name;
)
//能够访问private权限对象
istream& read(istream& is,MyClass m) { is>>m.name; return is;}

如果类定义在头文件中,需要为友元的函数在类外重新声明。

友元类

是指一个类A中定义为另一个类B的友元类后,可以在B的成员函数中访问A的私有对象。

class A{
	friend class B;//也可以只为成员函数定义友元friend void B::addA(int);
    int a;
}
class B{
public:
    A a;
    void addA(int n){ a.a+=n;}//由于定义了友元类 因此B可以访问到a
}

友元特性不能传递也不能继承,B是A的友元,C是B的友元,但是C不是A的友元,同理B是A的友元,C是A的派生类,B不能访问C中C的成员(但是可以访问C中A的成员

可变成员mutable

与const相反的一种成员变量,可以在const成员函数中进行修改。

运算符重载

//这两个函数相当于重载了>>,<<
istream& read(istream& is,mclass m);//read(cin,obj);
ostream& print(ostream& os,const myclass m);//print(cout,obj);

返回*this的成员函数

MyClass& Get(){ return *this; }//相当于返回该对象本身(引用),从而可以级联调用
myclass.Get().Other();//均在myclass对象上调用

MyClass Get(){ return *this; }//相当于返回该对象的副本

如果返回的是一个const引用的*this(成员函数本身是const,或者其返回值是const),那么最终会得到一个const对象,从而无法级联使用非const对象

类的隐式转换

对于提供单参数构造函数的类,可以直接把该参数类型的变量/值隐式转换成类对象,常常用在函数传递参数当中。

class Double{public:double db;Double(double v=0):db(v){}}
void func(Double db);
func(10);//会直接创建一个Double对象

可以通过explicit修饰单参数构造函数来避免隐式转换,该关键字只允许使用在类内部的构造函数中

静态成员

静态成员为类的所有对象所共享的成员static,可以通过类名::静态成员来调用

静态成员变量,静态成员变量的定义只能在类外定义

静态成员函数,不能使用this指针,不能定义为const

访问规则:非静态成员函数可以访问静态,非静态成员,静态成员函数只能访问静态成员

静态成员可以是不完全类型,也可以作为默认构造函数的参数

继承

一个类作为另一个类的派生类

class base_class
{
static:
    int static_val;//派生结构中只会存在一个静态变量
protected:
    int a;//protected关键字表示保护类型,为派生类提供访问权限
}
class sub_class:public base_class
{
public:
	sub_class():base_class(){}//调用父类构造函数
}

父类指针,引用均可指向派生类的对象,从而实现多态。

必须通过指针或者引用指向派生类对象才能实现多态,仅仅父类对象指向派生类是无法实现多态的(与C#不同)

先进行继承结构中根类的构造再依次往下进行构造。(参数一层层的传递上去后,再一层层地进行构造)

final

类似sealed关键字,如果定义class为final,使得class无法被继承,如果定于函数为final表示其无法被覆盖

class MyClass final{}
class Base{
    void find() final{}
}

虚函数

声明虚函数,表示这个函数可以被派生类重写,并且产生动态绑定。虚函数可以有具体的定义,可以有默认参数,派生类如果重写相应虚函数,返回值与形参必须与基类中的虚函数一致。

//在base中定义为了virtual,则其所有派生类就都是virtual的了
virtual void func(){}
//在sub_base中
virtual void func() override { base::func();}//基类名称::虚函数 调用基类函数

Base* base = new SubBase;
base->Base::func();//强行调用基类函数,无视虚函数动态绑定

通过将子类对象赋给父类的指针或者引用,父类对象也只能使用子类重写父类的哪些函数,而不能调用子类自己的函数。可以通过访问虚函数表的形式强行调用子类中新定义的那些虚函数,使用这个方法甚至可以访问定义为private和protected的虚函数

虚函数的实现

虚函数表

运行期间的多态通过虚函数与虚函数表来实现。

对于一个包含虚函数的类对象或者继承的父类中含有虚函数的对象来说,其内存中,首个位置存储的即是虚函数表(指向虚函数表的一个指针),可以通过如下操作得到

base b;
cout<<(int*)(&b)<<endl;//返回虚函数表地址

在实现多态的过程中(通过基类指针调用派生类成员函数时),基类指针会遍历虚函数表看是否存在满足条件可调用的函数。

单继承情况下的虚函数

如果不存在子类重写父类虚函数时,生成子类对象,其虚函数表中即会存在父类的虚函数,又会存在子类的虚函数,子类的虚函数排在父类的虚函数后面,可以通过如下代码验证:

typedef void (*func)();
class Base{
public:
	virtual void b_func(){cout<<"base"<<endl;}
}
class Subase:public Base{
public:
	virtual void s_func(){cout<<"subase"<<endl;}
}
Subase sb;
(func)(*(int*)*(int*)(&sb))();//(int*)*(int*)(&sb)为虚函数表的第一项
(func)(*((int*)*(int*)(&sb)+1))();

如果子类重写了父类的虚函数,那么此时子类对象虚函数表中原本排在前面的父类虚函数会被重写的子类虚函数代替,重写的这个函数不会出现再后面。当指向该子类对象的父类指针再调用对应函数的时候,遍历虚函数表首先得到的是重写过后的函数,从而调用这个函数,实现了多态。

多重继承下的虚函数表

与单继承类似,区别在于多重继承的类对象其内存位置的头几项会按照继承顺序给出对应基类的虚函数表,如果存在重写,那么重写函数会代替虚函数表中原本基类的函数,没有重写的虚函数会添加到第一个虚函数表中。

安全性问题

可以通过访问内存的方式,在父类指针指向派生类对象的时候,访问其私有权限或者保护权限的虚函数。

抽象基类

纯虚函数,不能在类内给出定义,不能给出默认参数,定义了纯虚函数的类为抽象基类,抽象基类不能创建对象,继承了抽象基类的函数必须给出纯虚函数的定义,否则依旧是抽象基类。但是抽象基类是可以定义指针与引用的

C++不像C#是没有abstract关键字的,只要有纯虚函数就是抽象基类

格式如下;

//定义了纯虚函数的类就是抽象基类
class abstract{
	virtual void func()=0;//virtual 函数定义 =0即为纯虚函数
}
//抽象基类中其他的函数是可以给出定义的

调用构造函数的顺序是从上至下调用

函数的调用与查找

调用类对象函数时,会从当前类的作用域(包含在其基类作用域内),从下往上寻找其基类的作用域,查找有无满足调用名称的函数,再根据其是一般函数调用还是虚函数调用进行检查。

因此,如果虚函数重写时函数定义不一致会导致找不到的情况。

继承中的拷贝控制

虚析构函数

对于一个继承结构来说,基类析构函数需要定义为虚析构函数,这是为了使基类指针指向派生类,delete时能销毁正确的类型。

构造函数

构造函数不能为虚拟构造函数,因为构造函数的调用顺序本身就是沿着继承结构自上而下的

如果一个派生类对象赋值/初始化一个基类对象会调用基类的赋值运算符/拷贝构造函数。

继承中的类型转换

RTTI:运行时类型识别,可以在运行的过程中,得到对象的类型。

**dynamic_cast:能够将基类指针或者引用,安全地转换成派生类。**比如基类A指向了B,创建一个新B变量,将A转换到B。只能转指针或者引用

A* a=new B;
B* b=dynamic_cast<B*>(a);//将a强制转换成b

typeid:返回类型对于数据的类型

类似C#中的typeof,返回type_info对象,用来判定对象是否是给定类型

int a;
typeid(a)==typeid(int);//返回true

typeid在oop中的使用

如果使用typeid用来计算类型变量,类型指针那么直接得到对应类型的类型/类型指针。

如果使用typeid用来计算基类的解指针,根据基类有无虚函数分为如下两种情况

  1. 基类含虚函数,那么解指针将得到指针具体所指向的对象(如果指向派生类就返回派生类类型)
  2. 基类不含虚函数,那么解指针就得到基类类型,哪怕指向的是派生类

引用同解指针

综上,基类最好保持一个虚函数,如果不存在需要显示定义的虚函数,那么最好给出一个虚析构函数

多继承基础

C++支持多继承,多继承的构造函数调用顺序是其继承列表的先后顺序,从根向下调用构造函数

析构函数正好相反。

class BaseA{public: BaseA(){}}; class BaseB{public: BaseB()};
class Subase:public BaseA,public BaseB
{
public:
	Subase():BaseA(),BaseB(){}
}

拷贝控制会按照构造函数调用的顺序依次调用对应函数的合成/自定义拷贝控制

虚继承

**使得虚继承类的派生类能够共享同一个基类,**用来解决多重继承中,间接继承同一个类导致重复生成对象的问题。保证继承体系中的基类不重复。

格式:class MyClass: public virtual Base

虚继承只影响继承虚继承类的类,不影响虚继承类本身

class DataStruct{}
class Tree:public virtual DataStruct{}//虚继承
class Graph:public virtual DataStruct{}
class RBT:public Tree,public Graph{}

多重继承容易出现二义性,因此不提倡多重继承,A,C继承D,B继承A,C。A,C,D中同时含有x,B对象使用x时就产生了二义性。

虚继承的构造顺序

虚继承中的虚基类会在最底层的对象中进行构造,随后的基类构造函数调用顺序与一般继承类似。如果不显示给出就调用虚基类的默认构造函数。以上述代码为例构造函数顺序如下:

DataStruct,Tree,Graph,RBT

如果存在多个虚基类,那么调用顺序按照继承列的顺序先调用虚基类构造函数

C++关键系统调用

文件输入输出

头文件fstream

涉及的三个类型fstream,istream,ostream,使用方式如下

//输入(是指输入进程)
istream streami("文件名称",fstream::in);//指明文件名称的构造函数自动调用了open函数
streami>>show;//如果打开.txt文件,那么输入的就一定是字符串,仅使用>>遇到空格就不读取了
streami.close();//如果切换打开文件需要先关闭文件流
streami.open("text.txt",fstream::in);//打开新文件
while(getline(streami,show)) //getline每次读取一行,遇到回车就下一行,直到文件末尾
    cout<<show<<endl;
streami.close();

//输出(是指从进程输出到文件)
ostream streamf("文件名称",fstream::app);
//输出的文件模式有out(会清空内容),app(向文件中添加内容),trunc(文件截断)
for(int i=0;i<n;i++)
{
    streamf<<out;
    //为方便文件读取,每次输入完后还跟一个'/n
    streamf<<'/n';
}
streamf.close();

内存管理

C++内存分区一共抽象为5个区域

代码区

全局数据区(全局,静态变量)

常量区

堆区(动态变量) 栈区(局部变量)

栈区与堆区的区别

栈上用来存储局部变量,包括函数传递的参数,返回值等等,由系统来释放内存。

堆是用来动态地给变量分配内存的,在C++中,动态分配内存以及释放内存需要程序员自行管理。

new,delete

new与malloc区别

  1. new是运算符 malloc是库函数
  2. new会调用构造函数,malloc只会申请内存
  3. new返回指定类型的指针,malloc返回void指针
  4. new可以自动计算所需空间大小,malloc需要手动设置空间
  5. new可以被重载

new->operator_new()->malloc()->构造函数

delete与free区别

  1. delete是运算符 free是库函数
  2. delete会调用析构函数,free会释放内存
  3. free要检查是否为空,delete则不会

delete->析构函数->operator_delete()->free()

delete只能删除动态分配的空间,在delete后,如果还需要使用该指针需要为其赋值nullptr,否则指针空悬

NULL与nullptr区别

内存对齐

所谓内存对齐是指CPU指令操作的内存地址能够被其操作的内存大小整除。

对于内置类型来说对齐量就是其大小

对于自定义类型来说对齐量是其非静态成员变量中最大的那个变量,如果有些成员变量不满足对齐量要求,会进行空白填充。

class MyClass
{
public:
	int a;//占4字节
    char ch;//占1字节
	short s;//占2字节
};
//因此对齐量是4字节,对齐量必定是2的倍数
//a大小直接满足对齐要求占第1,2,3,4个字节
//c占第5个字节,为了满足对齐要求,空白6,7,8字节
//s同理,满足9,10字节后空白11,12字节
//因此最终大小是12

可以通过alignas来显示指定对齐量

class alignas(16) MyClass{
public:
	int a;//占4字节
    char ch;//占1字节
	short s;//占2字节
}
//这个对象最对齐量是16,直接把大小扩展到16

之所以要内存对齐是由于内存不对齐,CPU的性能可能会降低甚至报错

对齐量不能设置小于默认对齐

内存泄漏的原因

  1. 类的构造函数与析构函数中没有调用匹配的new,delete函数,导致对象被销毁后,成员指针所指向的空间没有被释放。
  2. 释放数组时没有使用方括号
  3. 释放指针对象数组时,仅仅使用方括号,销毁的是指针占用的空间,而没有销毁指针所指向对象占用的空间,需要循环处理
  4. 没有将基类的构造函数定义为虚函数

RALL

将对象的资源管理和对象的生命周期绑定。智能指针可以说是RALL的一种体现,在对象生命周期结束时同时释放资源,补足了C++本身容易导致内存泄漏的短板。

静态链接与动态链接

静态库函数的链接是放在编译时完成的

动态库把对一些库函数的链接载入推迟到程序运行时

两者都是共享代码程序复用的方式

静态链接优点:方便移植,程序运行时静态链接库以及已经完成遍历了,编写使用方便

动态链接优点:避免浪费内存空间,如果不同的应用程序使用相同的dll,那么内存中只需要有一份该共享库的实例。可以实现进程的资源共享,且方便升级。

C++泛型编程

模板编程是泛型编程的基础,所谓泛型编程指的可以根据传递的类型作为参数来设计程序。

主要用于设计函数与类类型

//设计函数
template<typename T,typename Q>
T func(T t,Q q){}

//设计类类型
template<typename T>
class MyClass{
public:
    T feature;
    void show(T t){}
}
相关标签: C++ c++