c++面经突击(简版)9.24

编译内存相关

常量与变量

1.变量

变量提供了一个拥有名字和为程序提供数据的可存储空间。而变量的数据类型决定着变量所能占据的内存空间的大小、数据怎么存储、被存储数据值的范围以及所能参与的运算。一般情况下,变量和对象这两个名词是等价的,均指一块能存储数据并具有某种类型的内存空间。

有关声明与定义:

由于C++支持分离式编译,也就是可以将程序分为多个文件来进行编译,当一个文件中需要使用另一个文件中定义的变量时,就需要先告知程序使用使用的是哪个定义的变量。 声明就是通告程序变量的名字,使得程序可以使用别处定义的变量。定义是创建与名字相关的实体(开辟内存空间,甚至赋初值)。声明和定义均规定了变量的类型和名字。

2.常量

分类如下:

(1)字面值常量

字面值常量就是我们一看它就知道它的值的常量,如42,0.5,‘a’,“abc”,true,false等。
给字面值常量添加前缀或者后缀,可以改变其默认类型:
字符和字面值常量:U, u, L, u8
整型字面值:u , U;l , L;ll , LL
浮点型字面值:F, f; l, L

(2)const限定符

利用关键字const对变量的类型加以限定,使对象变为常量,类似于C语言中的define功能。const对象只能执行不改变内容的操作。
const int bufsize = 1024;

1)初始化

const对象一旦创建后对象的值就不能改变,因此 const对象必须初始化。在对const对象进行初始化时,不必考虑用来初始化const对象的值是否是常量。

// 1.
const int a = 1;
// 2.
int b = 2;
const int c = b;
2)对常量的引用

对常量的引用进行初始化时,只要初始值可以转换为引用的类型即可,不必考虑被引用的类型是否是常量。也就是说,常量引用可以引用一个非常量,但是与普通的引用不同的是不能通过常量引用来改变对象的值

// 1.
int a = 1;
const int& b = a;
// 2.
const int c = 1;
const int& d = c;//表示这个引用(d)只能与c同名了:只代表c的这一块内存了

常量引用的理解:“常量引用”其实是“对 const 的引用”的简称。顾名思义,它把它所指向的对象看作是常量(不一定是常量),因此不可以通过该引用来修改它所指向的对象的值

严格来说,并不存在常量引用,因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于 C++ 语言并不允许随意改变引用所绑定的对象,所以从这层意思上理解所有的引用又都算是常量。与普通引用不同的是,“常量引用”(即对 const 的引用)不能被用作修改它所绑定的对象。

3)指向常量的指针

对于常量的地址,只能使用指向常量的指针。而指向常量的指针也可以指向非常量,与对常量的引用类似,不能通过该指针改变对象的值,但不限制用其他方式改变对象的值。

//1.
const int a = 1;
int* aPtr = &a;
4)常量指针

指针本身就是一个对象,因此可以将指针定义为常量。如果常量指针指向的是一个非常量,那么可以通过该指针改变对象的值。

5)顶层const和底层const

顶层const可以表示任何对象是常量,而底层const常与指针和引用等复合类型有关。当执行对象的拷贝操作时,拷入和拷出的对象都必须具有相同的底层const资格,或者两个对象的类型能够相互转换。

int i = 0;
const int *const p = i;//左边的const是底层const,右边的const是顶层const
6)constexpr和常量表达式

常量表达式指值不会改变并且在编译过过程就能得到就计算结果的表达式。字面值常量属于常量表达式;用常量表达式初始化的const对象也是常量表达式。可以将变量声明为constexpr类型,而且必须用常量表达式来初始化。
限定符constexpr作用于指针时,是将指针定为常量而不是指针指向的对象,这与const限定符的作用是不同的。

全局/局部变量、静态/非静态变量(可以不看,只看下面那个)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HERuoyQ0-1632487370835)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210913185449744.png)]

局部变量:定义在函数内部的变量(函数的形参也是局部变量),只能在定义它的函数内部使用
全局变量:定义在函数外面的变量,所有函数都可以使用
静态变量:1.全局变量 2.前面加了“static”关键字的局部变量

变量的分类(可以不看上面那个,只看这个)

对变量的分类依据可以把握两点:作用域(链接性)、存储位置(决定其生命周期)。

下面对链接性进行解释:

链接性 作用域
外部链接性 全局作用域
内部链接性 文件作用域
无连接性 局部作用域(函数或代码块内部)

下面对存储位置进行解释:

  1. 存储在栈:短生命周期
  2. 存储在静态存储区:长生命周期

1.自动变量——栈区局部变量(短生命周期)

  1. 作用域:局部——函数或代码块内部
  2. 链接性:无链接性
  3. 存储位置:栈
  4. 生命周期:短
  5. 如何产生:直接定义——如:int a;
  6. 编译器是否为自动为其初始化(若代码未完成初始化):不会。(其实是执行时才会创建,所以编译器不能再为其初始化了)

2.静态变量——静态存储区变量(长生命周期)

(1)全局变量——静态存储区全局变量

  1. 作用域:全局作用域——外部文件也可以访问
  2. 链接性:外部链接性
  3. 存储位置:静态存储区(.bss或.data)
  4. 生命周期:长
  5. 如何产生:在函数之外定义——int a;
  6. 编译器是否为自动为其初始化(若代码未完成初始化):会。如对于整数而言,会自动初始化为0,并存入.bss区。若人工初始化为0也会存入.bss,其他的人工初始化会存入.data区。

(2)静态局部变量——静态存储区局部变量

  1. 作用域:局部作用域(只能在函数内或代码块内被访问)
  2. 链接性:无链接性
  3. 存储位置:静态存储区(.bss或.data)
  4. 生命周期:长
  5. 如何产生:在函数内或代码块内部定义且要加上static关键字——static int a;
  6. 编译器是否为自动为其初始化(若代码未完成初始化):会。其他相关同上。

(3)静态全局变量——静态存储区文件变量

  1. 作用域:文件作用域(只能在本文件中被访问)
  2. 链接性:内部链接性
  3. 存储位置:静态存储区(.bss或.data)
  4. 生命周期:长
  5. 如何产生:在函数外定义且要加上static关键字——static int a;
  6. 编译器是否为自动为其初始化(若代码未完成初始化):会。其他相关同上。

3.两者的实质性差别

实质性差别在于存储位置:栈 or 静态存储区

编译器对两者的处理不一样,对自动变量,采用栈;对静态变量,编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序的执行期间一直存在。

静态函数

static修饰的函数叫做静态函数,有两种静态函数:静态成员函数+全局静态函数

1.静态成员函数:是类的方法(出现在类里)

根据静态的相关特点来推出静态成员函数的特点:

(1)存放在固定存储区:

调用这个函数不会访问或者修改任何对象(非static)数据成员。

理解:类的静态成员(变量和方法)属于类本身,在加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。

2.全局静态函数:不出现在类中

  1. 作用域:文件作用域

这样的static函数与普通函数的区别是:用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说它可以被其它代码文件调用。

在函数的返回类型前加上关键字static,函数就被定义成为静态函数。普通 函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。因此定义静态函数有以下好处:
  <1> 其他文件中可以定义相同名字的函数,不会发生冲突。
  <2> 静态函数不能被其他文件所用。

变量的两种声明方式

1.定义声明

给变量分配存储空间,可进行初始化,有两种方式:

  1. int a = 1;
  2. extern int a = 1;

2.引用声明

不给变量分配存储空间

如果在多个文件中使用外部变量(全局,且具有外部链接性),只需在一个文件中包含该变量的定义(重复定义也会出现问题),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。

编译预处理

预处理器进行编译预处理——

  • 预编译处理指令:
    • 实际上不是C++语言的一部分,它只是用来扩充C++程序设计的环境
    • 以“#”来引导,每一条预处理指令单独占用一行,不要用分号结束
  • 预处理操作符

典型的预处理指令:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZC9hIIa0-1632487370837)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914133740644.png)]

分类

c++提供的编译预处理主要有三种:

1. 宏定义

C++ 宏定义将一个标识符定义为一个字符串,源程序中的该标识符均以指定的字符串来代替。因此预处理命令后通常不加分号。这并不是说所有的预处理命令后都不能有分号出现。由于宏定义只是用宏名对一个字符串进行简单的替换,因此如果在宏定义命令后加了分号,将会连同分号一起进行置换。

习惯:

  1. 在书写#define 命令时,注意<宏名>和<字符串>之间用空格分开,而不是用等号连接。
  2. 使用#define定义的标识符不是变量,它只用作宏替换,因此不占有内存。
  3. 习惯上用大写字母表示<宏名>,这只是一种习惯的约定,其目的是为了与变量名区分,因为变量名通常用小写字母。
  4. 如果某一个标识符被定义为宏名后,在取消该宏定义之前,不允许重新对它进行宏定义。取消宏定义使用如下命令:#undef<标识符>
  5. 宏定义可以嵌套,已被定义的标识符可以用来定义新的标识符。
(1)#define指令

使用方式:#define <宏标识符> <字符串>

在后面的源码中,用这些字符串来代替宏标识符。

  • 宏表示的值可以是一个常量表达式,其中允许包括前面已经定义的宏标识符。

    #define ONE 1
    #define TWO 2
    #define THREE (ONE+TWO)
    //有关括号:不是必须的
    six=THREE*TWO ——> six=(ONE+TWO)*TWO//此时如果没了括号就会计算错误,因为宏定义的实质是字符串的替换!!!
    
  • 宏还可以代表一个字符串常量

#define VERSION "Version 1.0 Copyright(c) 2003"
  • 宏也可以代表一个常数。

    #define SECONDS_PER_YEAR (60*60*24*365)UL
    //表达式将使一个16位机的整形数溢出,因此要用到长整型符号L,告诉编译器该数是一个无符号长整型数UL
    
(2)带参数的#define指令

带参数的宏和函数调用看起来有些相似。

举例:#define Cube(x) (x)*(x)*(x)

可以时任何数字表达式甚至函数调用来代替参数x。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:

#define Cube(x) (x)*(x)*(x)
int num=8+2;
volume=Cube(num);

不安全的用法:

volume=Cube(num++);
//展开之后如下:
volume=(num++)*(num++)*(num++);//结果是10*11*12,而不是10*10*10;
(3)#运算符

出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。

#define PASTE(n) "adhfkj"#n
main(){
	printf("%s\n",PASTE(15));
}

宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15。

(4)##运算符

##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。

#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c
main(){
    printf("%d\n",NUM(1,2,3));
    printf("%s\n",STR("aa","bb","cc"));
}
//最后程序的输出为:
//123
//aabbcc

2. 文件包含

#include

3. 条件编译

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAjZexN8-1632487370838)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160206744.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndDOiu3q-1632487370840)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160219745.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6Qn5STx-1632487370841)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160235439.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lFaSWLGg-1632487370842)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160249413.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSO8ekUI-1632487370843)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160310430.png)]

4. # pragma once

指定当前文件在编译时只包含一次。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ggj1cDP7-1632487370844)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160558468.png)]

把类的对象创建限制在堆或栈

首先要明白的是对象创建在堆和栈的区别:

  1. 创建在栈:代码直接调用类的构造函数,此时则要由编译器负责对象的创建(考虑构造函数)和删除(考虑析构函数)。
  2. 创建在堆:代码不直接调用类的构造函数,而是进入底层之后才会调用构造函数。但是程序员必须负责delete,一般都是程序员负责new和delete,编译器不会自动负责对象的删除。

明白了以上两点,对对象创建位置的限制便好理解了。

1.把类的对象创建限制在堆

创建在堆,一般情况是要由程序员负责new(底层调用构造函数)和delete(底层调用析构函数)一个类的对象。若程序员没有new,如何让其也创建在堆上呢。

最直观的思想:避免直接调用类的构造函数。

(1)解决方法1:将析构函数设置为私有

当程序员直接调用类的构造函数时,理论上此时编译器要准备把该对象创建在栈上并负责该对象的删除(析构函数),因此编译器创建在栈上时要检查该对象的析构函数(其实是该对象的非静态成员进行检查,因为需要静态成员是建立在静态存储区的)能否被访问,此时发现不能访问析构函数,则编译器不会把这个对象创建在栈上。

因为析构函数是私有的,所以编译器不能创建在栈,因此对象的删除不能由编译器完成。此时需要一个public的destroy()函数,负责对象的删除。示例如下:

class A
{
public:
    A() {}
    void destory()
    {
        delete this;
    }

private:
    ~A()
    {
    }
};

(2)解决方法2:构造函数设置为 protected

构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。

class A
{
protected:
    A() {}
    ~A() {}

public:
    static A *create()//设置为静态保证可以在只有类的时候便调用,否则产生对象了才能调用
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};

2.把类的对象创建限制在栈上

将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。

class A
{
private:
    void *operator new(size_t t) {}    // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
    A() {}
    ~A() {}
};

内存对齐(可以详细去看面经,注意下面的评论总结)

什么是内存对齐

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

大家都知道,C++空类的内存大小为1字节,为了保证其对象拥有彼此独立的内存地址。非空类的大小与类中非静态成员变量和虚函数表的多少有关。

而值得注意的是,类中非静态成员变量的大小与编译器内存对齐的设置有关。

成员变量在类中的内存存储并不一定是连续的。它是按照编译器的设置,按照内存块来存储的,这个内存块大小的取值,就是内存对齐。

为什么要进行内存对齐

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

内存对齐规则

我们以对于结构体的内存对齐为例,描述内存对齐的规则:

  1. 每个数据成员的存放首位置:第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
  2. 结构体的存放首位置以及结构体大小:在数据成员完成各自对齐之后,类(结构或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

注:很明显#pragma pack(n)作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n的缺省数值是按照编译器自身设置,一般为8,合法的数值分别是1、2、4、8、16。即编译器只会按照1、2、4、8、16的方式分割内存。若n为其他值,是无效的。

初始化和赋值的对比

对于简单的数据类型,如int等,初始化和赋值是没有什么区别的。但对于比较复杂的自定义的一些结构,比如String、类等,初始化和赋值是有较大区别的。我们以类为例把初始化和赋值两者进行对比:

1.初始化

类的构造函数(默认构造函数、拷贝构造函数等)其实质上是服务于类的对象的初始化的,即我们在创建一个类的对象的时候,确定该对象的数据成员的值。该初始化都是在类的构造函数中完成的。有几种方式完成对类的初始化:

class A{
private:
    int x;
    int y;
public:
	A(){
		x = 1;
        y = 1;
    };
    ~A();
}
A a1;//调用无参构造函数
A a2 = a1;//调用默认拷贝构造函数。这里的“=”代表初始化,不是赋值。
A a3(a1);//调用默认拷贝构造函数

2.赋值

为类中的“=”进行重写之后,可以直接对类进行赋值,在这里我们以String为例:

class  String  {
public:
    String( const char *init ); // intentionally not explicit!
    ~String();
    String( const String &that );
    String &operator =( const String &that );
    String &operator =( const char *str );
    void swap( String &that );
    friend const String // concatenate
        operator +( const String &, const String & );
    friend bool operator <( const String &, const String & );
    //...
private:
    String( const char *, const char * ); // computational
    char *s_;
} ;
//拷贝构造函数
String::String(  const   char   * init )  {
    if( !init ) 
        init = "";
    s_ = new char[ strlen(init)+1 ];
    strcpy( s_, init );
}
//析构函数
String:: ~ String()  { delete [] s_; }
//赋值操作
String  & String:: operator   = (  const   char   * str )  {
    if( !str ) 
        str = "";
    char *tmp = strcpy( new char[ strlen(str)+1 ], str );  // 多了中间变量
    delete [] s_; // 多了删除s_;
    s_ = tmp;   // 多一个赋值操作!现在是指向字符的指针,如果是个大对象,效率的差别可想而知.
    return *this;
}

类的构造函数和析构函数

1.构造函数

默认构造函数:在程序员没有重载构造函数时,编译器自动有一个默认无参构造函数。若程序员定义了构造函数(无论有参还是无参),编译器都不会再有无参构造函数了。

2.析构函数

除了需要知道在对象被销毁时会自动调用析构函数外,还应注意以下事项:

  1. 像构造函数一样,析构函数没有返回类型。
  2. 析构函数不能接收实参,因此它们从不具有形参列表
  3. 由于析构函数不能接收实参,因此只能有一个析构函数

父类与子类的构造函数与析构函数的关系

1.顺序

创建并删除一个子类对象时,函数的调用顺序如下:

  1. 父类构造函数
  2. 数据成员对象的构造函数
  3. 子类构造函数
  4. 子类析构函数
  5. 数据成员对象的析构函数
  6. 父类析构函数

2.构造函数

子类不能继承父类的构造函数,需要定义自己的构造函数。因此构造函数也不能重写。

在不显式声明的情况下,子类默认调用父类的无参构造函数。因此若父类只定义了一个有参构造函数(只要父类定义了构造函数(无论有参还是无参),就不会再有默认无参构造函数了),则在调用时便会报错。因此,此时子类的构造函数要显式表明要调用父类的哪一个构造函数(同时需要确定父类构造函数的参数是什么),代码示例如下:

class Base {
public:
    Base(int argue){
        cout << "我是Base有参构造函数!"<

3.析构函数

析构函数可以重写并且建议重写(virtual)。

复制构造函数(拷贝构造函数)

1.什么叫对象的拷贝

什么叫对象的拷贝:用其它对象的数据来初始化新对象的内存(因此也是构造函数)。

严格来说,对象的创建包括两个阶段,首先要分配内存空间,然后再进行初始化:

  • 分配内存很好理解,就是在堆区、栈区或者全局数据区留出足够多的字节。这个时候的内存还比较“原始”,没有被“教化”,它所包含的数据一般是零值或者随机值,没有实际的意义。
  • 初始化就是首次对内存赋值,让它的数据有意义。注意是首次赋值,再次赋值不叫初始化。初始化的时候还可以为对象分配其他的资源(打开文件、连接网络、动态分配内存等),或者提前进行一些计算(根据价格和数量计算出总价、根据长度和宽度计算出矩形的面积等)等。说白了,初始化就是调用构造函数。

2.拷贝构造函数的参数

**拷贝构造函数的参数:**本类的const引用。

(1) 为什么必须是当前类的引用呢?

如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

(2) 为什么是 const 引用呢?

拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

参数:本类的引用,可以是const,可以不是const。(复制构造函数的参数加不加 const 对某些程序來说都一样。但加上 const 是更好的做法,这样复制构造函数才能接受常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。注意:非常量对象可以转换为常量对象,但是常量对象不能转换为非常量对象。

默认复制构造函数:如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。

注意,默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。

3.深拷贝和浅拷贝

默认的是浅拷贝,只拷贝一个副本。

对于动态分配内存,需要是深拷贝的。如果有指针,一般都是深拷贝。

有关重写

有两种方法:普通重写和虚方法重写。

1.普通重写:不带virtual

2.虚方法重写:带virtual

智能指针(还是要仔细看看的,第一遍看好像没有特别理解)

C++里面的四个智能指针: auto_ptr(已被c++11弃用), unique_ptr(c++11支持),shared_ptr(c++11支持), weak_ptr(c++11支持) 其中后三个是C++11支持,并且第一个已经被C++11弃用。

1.智能指针相关介绍

(1)用途

智能指针主要用于管理在堆上分配的内存,其相关处理方式可防止内存泄漏。

(2)相关原理

智能指针将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

(3)为什么要使用智能指针:防止内存泄漏

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间

2. auto_ptr

采用所有权模式。

代码示例:

auto_ptr p1 (new string ("I reigned lonely as a cloud.")); 
auto_ptr p2; 
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。

auto_ptr的缺点:存在潜在的内存崩溃问题!

3. unique_ptr

unique_ptr实现独占式拥有严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免内存泄露特别有用。

采用所有权模式。

unique_ptr p3 (new string ("auto"));   //#4
unique_ptr p4;                       //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。尝试复制p3时会编译期出错,而auto_ptr能通过编译期从而在运行期埋下出错的隐患。因此,unique_ptr比auto_ptr更安全

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值(该unique_ptr还没有指针指向),编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,代码展示如下:

unique_ptr pu1(new string ("hello world")); 
unique_ptr pu2; 
pu2 = pu1;                                      // #1 不允许
unique_ptr pu3; 
pu3 = unique_ptr(new string ("You"));   // #2 允许    临时右值

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

注:如果确实想执行类似与#1的操作,要安全地重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。尽管转移所有权后还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。

unique_ptr ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

注:事实上在执行完move函数之后,其内部把ps1指向了null(防止ps1变成野指针)。

4. shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数

  1. use_count 返回引用计数的个数
  2. unique 返回是否是独占所有权( use_count 为 1)
  3. swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
  4. reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
  5. get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.

下面给出一个简单的代码示例:

int main()
{
	string *s1 = new string("s1");
 
	shared_ptr ps1(s1);//把普通指针转化为shared_ptr
	shared_ptr ps2;
	ps2 = ps1;//两个指针共享同一片内存
 
	cout << ps1.use_count() << endl;	//2
	cout<< ps2.use_count() << endl;	//2
	cout << ps1.unique() << endl;	//0
 
	string *s3 = new string("s3");
	shared_ptr ps3(s3);
 
	cout << (ps1.get()) << endl;	//033AEB48
	cout << ps3.get() << endl;	//033B2C50
	swap(ps1, ps3);	//交换所拥有的对象
	cout << (ps1.get())<

shared_ptr存在的内存泄漏的情况

两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

5. weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题——如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

代码示例如下:

class B;	//声明
class A
{
public:
	shared_ptr pb_;
	~A()
	{
		cout << "A delete\n";
	}
};
 
class B
{
public:
	shared_ptr pa_;
	~B()
	{
		cout << "B delete\n";
	}
};
 
void fun()
{
	shared_ptr pb(new B());
	shared_ptr pa(new A());
	cout << pb.use_count() << endl;	//1
	cout << pa.use_count() << endl;	//1
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << pb.use_count() << endl;	//2
	cout << pa.use_count() << endl;	//2
}
 
int main()
{
	fun();
	return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_,改为weak_ptr pb_ ,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。

注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(),因为pb_是一个weak_ptr,应该先把它转化为shared_ptr

shared_ptr p = pa->pb_.lock();
p->print();

weak_ptr 没有重载*和->,但可以使用 lock 获得一个可用的 shared_ptr 对象. 注意, weak_ptr 在使用前需要检查合法性.

expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.

lock 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.

use_count 返回与 shared_ptr 共享的对象的引用计数.

reset 将 weak_ptr 置空.

weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数.

语言对比(C++和其他语言)

C++11新特性:lambda表达式

lambda表达式的语法形式定义如下:

[ capture ] ( params ) opt -> ret { body; };

其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型,body是函数体。

代码举例:

auto f = [](int a) -> int { return a + 1; };
std::cout << f(1) << std::endl;  // 输出: 2

1.关于捕获列表

lambda 表达式还可以通过捕获列表捕获一定范围内的变量:

  • [] 不捕获任何变量。
  • [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  • [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
  • [=,&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,捕获所有外部变量(包括this)
        auto x3 = [&]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x4 = [this]{ return i_; };                // OK,捕获this指针
        auto x5 = [this]{ return i_ + x + y; };        // error,没有捕获x、y
        auto x6 = [this, x, y]{ return i_ + x + y; };  // OK,捕获this指针、x、y
        auto x7 = [this]{ return i_++; };              // OK,捕获this指针,并修改成员的值
    }
};
int a = 0, b = 1;
auto f1 = []{ return a; };               // error,没有捕获外部变量
auto f2 = [&]{ return a++; };            // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; };              // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; };            // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; };          // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); };  // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); };  // OK,捕获所有外部变量和b的引用,并对b做自加运算

如上:lambda 表达式的捕获列表精细地控制了 lambda 表达式能够访问的外部变量,以及如何访问这些变量。

2.lambda表达式的延迟调用

int a = 0;
auto f = [=]{ return a; };      // 按值捕获外部变量
a += 1;                         // a被修改了
std::cout << f() << std::endl;  // 输出0

在这个例子中,lambda 表达式按值捕获了所有外部变量。**在捕获的一瞬间,a 的值就已经被复制到f中了。**之后 a 被修改,但此时 f 中存储的 a 仍然还是捕获时的值,因此,最终输出结果是 0。

如果希望 lambda 表达式在调用时能够即时访问外部变量,我们应当使用引用方式捕获。

3.mutable显式指明

按值捕获得到的外部变量值是在 lambda 表达式定义时的值。此时所有外部变量均被复制了一份存储在 lambda 表达式变量中。此时虽然修改 lambda 表达式中的这些外部变量并不会真正影响到外部,我们却仍然无法修改它们。——>会报错的!

那么如果希望去修改按值捕获的外部变量应当怎么办呢?这时,需要显式指明 lambda 表达式为 mutable:

int a = 0;
auto f1 = [=]{ return a++; };             // error,修改按值捕获的外部变量
auto f2 = [=]() mutable { return a++; };  // OK,mutable

注意:被 mutable 修饰的 lambda 表达式就算没有参数也要写明参数列表。

4.lambda表达式的类型——闭包类型(Closure Type),一个特殊的,匿名的非 nunion 的类类型

我们可以认为它是一个带有 operator() 的类,即仿函数。

存储lambda表达式:std::function

操作lambda表达式:std::bind()

std::function  f1 = [](int a){ return a; };
std::function f2 = std::bind([](int a){ return a; }, 123);

C++11 lambda表达式精讲 (biancheng.net)

再看这个网站吧

面向对象

面向对象的三大特性

面向对象的三大特性:

  • 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
  • 继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改
  • 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。(派生类对象的地址可以赋值给基类指针,通过基类指针调用基类和派生类同名、同参数表的虚函数语句,编译时不确定要执行的是基类的还是派生类的虚函数,运行时执行到该语句时,如果基类指针指向的是基类对象,则基类的虚函数被调用,如果指向的是派生类对象,则派生类的虚函数被调用。这个机制称为多态。)

类相关

重载、重写、重定义

1.重载overload

(1)作用域

同一个作用域中,如类成员函数之间的重载、全局函数之间的重载。

(2)函数特征

函数名相同、形参列表不同(参数类型、数目或顺序)、与返回类型无关。

与是否有virtual无关,有或无virtual都可以是重载,因为只看参数列表是否不同。

const有关,因为const实际上是形参为const,这导致形参列表可能不同。

(3)应用

  1. 不同的构造函数:无参构造函数、有参构造函数、拷贝构造函数。
  2. 运算符的重载

(4)注意事项

  1. 类的静态成员函数与普通成员函数可以形成重载。

2.重写\覆盖override

(1)作用域

在父类和子类中

(2)规则

函数名相同、形参列表都相同(参数类型、数目或顺序)、返回类型相同或协变。

  1. 函数的const属性必须一致:const的属性是作用在传入参数中的,因此也代表参数列表需要一致。

  2. 函数的返回类型必须相同或协变

    有关协变的介绍:

    1. 首先理解三个概念:

      • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
      • 逆变(contravariant),如果它逆转了子类型序关系。
      • 不变(invariant),如果上述两种均不适用。
    2. 在C++的虚函数重写中,派生类是基类的协变类型协变类型的模板依然保持相同的性质,比如vector和vector。(注意需要是指针,因为引用或非指针不存在协变一说)

    3. 应用协变的程序代码如下:

      //正确示例:应用协变类型到虚函数重载中
      #include 
      using namespace std;
      
      class Base
      {
      public:
          virtual Base* op()
          {
              cout << "Base";
              return nullptr;
          }
      };
      
      class Derive : public Base
      {
      public:
          Derive* op() override
          {
              cout << "Derive";
              return nullptr;
          }
      };
      
      int main()
      {
      
          Base *a = new Derive();
          a->op();
      }
      
    4. 非指针或引用不存在协变一事,下面的虚函数重写是错误的。

  3. 函数名称与参数列表一致。

  4. 虚函数不能是模板函数。(可以了解一下什么是模板函数)

    #include 
    using namespace std;
    template
    void Swap(T & x, T & y)
    {
        T tmp = x;
        x = y;
        y = tmp;
    }
    int main()
    {
        int n = 1, m = 2;
        Swap(n, m);  //编译器自动生成 void Swap (int &, int &)函数
        double f = 1.2, g = 2.3;
        Swap(f, g);  //编译器自动生成 void Swap (double &, double &)函数
        return 0;
    }
    
  5. 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)

  6. 被重写的函数不能是static的,必须是virtual的

(3)应用

  1. 多态的实现——重写
  2. 重写的函数可以通过**父类指针(父类指针指向子类对象)**访问。如果没有用virtual修饰函数,那么通过父类指针访问的仍然会是访问父类的函数。

3.重定义\隐藏

(1)作用域

不同作用域,比如派生类成员函数屏蔽与其同名的基类成员函数、类成员函数屏蔽全局外部函数。

(2)规则

只要函数名相同即可。

  • 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。否则就是重写了。

请注意,如果在派生类中存在与基类虚函数同返回值、同名且同形参的函数,则构成函数重写。

(3)应用

= new Derive();
a->op();
}
```

  1. 非指针或引用不存在协变一事,下面的虚函数重写是错误的。

  2. 函数名称与参数列表一致。

  3. 虚函数不能是模板函数。(可以了解一下什么是模板函数)

    #include 
    using namespace std;
    template
    void Swap(T & x, T & y)
    {
        T tmp = x;
        x = y;
        y = tmp;
    }
    int main()
    {
        int n = 1, m = 2;
        Swap(n, m);  //编译器自动生成 void Swap (int &, int &)函数
        double f = 1.2, g = 2.3;
        Swap(f, g);  //编译器自动生成 void Swap (double &, double &)函数
        return 0;
    }
    
  4. 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)

  5. 被重写的函数不能是static的,必须是virtual的

(3)应用

  1. 多态的实现——重写
  2. 重写的函数可以通过**父类指针(父类指针指向子类对象)**访问。如果没有用virtual修饰函数,那么通过父类指针访问的仍然会是访问父类的函数。

3.重定义\隐藏

(1)作用域

不同作用域,比如派生类成员函数屏蔽与其同名的基类成员函数、类成员函数屏蔽全局外部函数。

(2)规则

只要函数名相同即可。

  • 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。否则就是重写了。

请注意,如果在派生类中存在与基类虚函数同返回值、同名且同形参的函数,则构成函数重写。

(3)应用

你可能感兴趣的:(我的C++痛苦史,c++)