CPP LEARNING

1. new

  1. 使用new来开辟内存空间 delete来删除已开辟的空间
  2. new返回的是该数据类型的指针
  3. 使用delete释放数组时需要用 delete[]

2.引用

  1. 引用必须要初始化
  2. 初始化后引用就不能改了
  3. 不要返回局部变量的引用
  4. 返回值是引用的函数可以作左值
  5. 引用的本质在C++内部的实现就是一个指针常量
  6. 可以用const修饰防止形参修改实参

3.函数

1. 函数的默认参数

  1. C++的函数可以有默认参数,但是从第一个默认参数开始,后面的参数都必须有默认值
  2. 函数的声明和实现只能有一个有默认参数

2.函数的重载

  1. C++的函数可以同名,提高函数的复用性。
  2. 函数的重载必须在同一个作用域下,且名称相同。
  3. 函数的重载要求满足函数的参数或者类型不同,或者个数不同,或者顺序不同。 函数的返回值不能做函数重载的条件
  4. 引用可以做函数重载的条件 (const修饰与无const修饰) 传入常量区数据,调用const引用,传入其他数据,调用非const引用。
    5.当重载函数有默认参数时,默认参数可能会无法重载
1
2
3
4
5
6
7
8
9

void func2(int a,int b = 10)
{
cout<<"func2()的调用"<<endl;
}
void func2(int a)
{
cout<<"func2()的调用"<<endl;
}

func2(int a,int b = 10)具有一个默认参数,这使得func2(10)具有二义性


类和对象

C++ 面向对象三大特性 封装、继承、多态

1. 封装

将属性和行为写在一起,并加入一些权限控制。 ———— 类

struct不同的是 类——class的权限是默认私有的。
通过一个类来创建一个对象的过程称为 “实例化” 。

访问权限

  1. 公共 成员在类内可以访问 类外也可以访问
  2. 保护 成员在类内可以访问 类外不可以访问 父类的保护权限,子可以访问
  3. 私有 成员在类内可以访问 类外不可以访问 父类的私有权限,子也不可以访问 (继承)

构造函数和析构函数

使用构造函数来初始化,析构函数来清理。 构造函数和析构函数由编译器自动调用 但如果不自己实现,调用的是空实现。

构造函数

不需要写返回值类型,函数名称与类相同,可以有参数,可以发生重载。

在创建对象时,程序会自动调用构造函数,不需要手动调用,并且只会调用一次

析构函数

没有返回值,没有参数,函数名前要加上~

由于不可以有参数,所以不能发生重载。

和构造函数一样,析构函数会在对象销毁前自动调用析构函数。

构造和析构函数都是必须要有的 如果自己不提供,程序会提供空实现

构造函数的分类

按参数:有参构造和无参构造
按类型:普通构造和拷贝构造

构造函数的调用方式

  1. 括号法   person p(10);
  2. 显示法   person p = person(10);
  3. 隐式转换法 person p = {10,"name"};

person(10)创建的是一个匿名对象,匿名对象在当前行结束后就会被释放。显示法相当于给匿名对象找到一个名字。

不要利用拷贝构造函数来初始化一个匿名的对象。

拷贝构造函数的调用时机

C++中拷贝构造函数的调用时机通常有三种情况

  • 使用一个已经创建完的对象来初始化一个新对象person p(p0);
  • 以值传递的方式给函数参数传参 doWorK(p); 值传递的本质会拷贝一个临时副本 这个过程会调用拷贝构造函数
  • 以值方式返回局部对象
1
2
3
4
5
Person func()
{
person p1;
return p1;
}

函数内的p1在函数运行完就会被是放掉,return 的p1是创建的新变量。

构造函数的调用规则

C++会为一个类提供至少三个默认函数

  1. 默认构造函数(空实现)
  2. 默认析构函数(空实现)
  3. 默认拷贝构造函数 => 有默认内容(值拷贝)

如果你写了一个有参构造函数,C++不会提供默认无参构造,但会提供拷贝构造函数
如果你写了一个拷贝构造函数,C++就不会再提供默认的其他构造函数了

深浅拷贝

深拷贝 重新申请空间

浅拷贝 直接赋值

初始化列表

传统操作:

1
2
3
4
5
6
7
8
9
10
Person(int a_,int b_,int c_)
{
a = a_;
b = b_;
c = c_;
}

int a;
int b;
int c;
1
2
3
4
Person():a(10),b(20),c(30){}  //行参列表
int a;
int b;
int c;
1
2
3
4
Person(int a_,int b_,int c_):a(a_),b(b_),c(c_){}  //更灵活的写法
int a;
int b;
int c;

类对象作为类成员

1
2
3
4
5
class A{};
class B
{
A a;
}

当A作为B的成员时,创建一个类B的对象,会先创建一个类A的对象   [先有手机后有人]

静态成员变量

  • 所有对象共享同一份数据
  • 在编译阶段就分配内存 (还没有运行可执行文件前已经分好了)
  • 类内声明,类外初始化 (必需操作) (static修饰的成员变量在类外初始化时分配内存) (静态成员并不具体作用在某个对象上)
  • 静态成员不能在类内初始化
  • C++中声明和定义是有区别的,在类内进行的是成员变量的声明,而不是定义。所以在类外初始化静态成员变量时仍然要加上数据类型。
  • 由于静态成员变量不属于某个类,所以他有两种访问方式
  • 1.通过对象访问
  • 2.通过类名访问
1
person::static_member_
  • 静态成员变量也是有访问权限的 (类外访问不到静态成员变量的内容)

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量

成员函数的大小并不算在类里!成员变量和成员函数在存储上是分离的

静态成员变量的调用有两种访问方式

  1. 通过对象
  2. 通过类名

静态成员函数只能访问静态成员变量

  • 静态成员函数也是有访问权限的

C++对象模型和this指针

C++中 只有非静态成员变量才属于类的对象上

空对象占用的内存大小为 1 字节

C++编译器会给每个空对象也分配一个字节的空间,是为了区分每个对象占用的位置

不是存储对象的地址,而是把对象的地址占用住,防止别的对象存在同一地址


在C++中,空结构体的大小也是 1<br$$>
在C语言中 空结构体的大小是 0

static成员变量和对象中的成员变量是分开存储的,不算在对象的大小里 (存储在全局区)

this指针

非静态成员函数的实例只有一份,它通过this指针来区分是哪个对象调用的自己。
用途

  • 当形参和成员变量同名时,可以用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可用 return *this
    静态成员函数不具备 this 指针

空指针访问成员函数

C++的空指针也是可以访问成员函数的
但是要判断this指针是否为空 加强代码的健壮性

const修饰成员函数

常函数:

  • 成员函数后加 const 的函数叫做常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

  • 在声明对象时,在对象前面加const 来定义一个常对象
  • 常对象只能调用常函数
  • mutable在常对象下也适用

在成员函数后加const修饰的其实是this指针,让指针指向的值也不可以修改

常对象只能调用常函数的原理:常对象的this指针是常量指针,而this指针是作为隐含参数传给类内函数的,只有常函数的this指针是常量指针常量,可以接收常对象的this指针 (其实就是普通指针可以向const修饰的指针转换,而const修饰的指针不能向普通指针转换)

友元 friend

通过关键字friend可以让类外访问类内的私有属性 可以让一个函数或者类 访问另一个类中的私有成员

1
2
3
4
5
6
7
class Friend{};

friend func();

friend Friend;

friend Friend::func();

运算符重载

可以对已经存在的运算符进行重新定义,赋予另一种功能

  1. 加号运算符重载
  2. 左移运算符重载
  3. 递增运算符重载
  4. 赋值运算符重载
  5. 关系运算符重载
  6. 函数调用运算符重载 (仿函数)
    运算符可以以全局函数或成员函数的形式重载,拓展更多使用方法
    cout是 ostream 类的一个对象 通过成员函数 operator<< 来输出内容
    1
    my_integer& operator++(int)  //在()里写int以表明这是一个后置递增

2. 继承

基本语法

继承可以减少重复的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base      // 父类 (基类)
{
void func()
{
cout<<"func()"<<endl;
}
};

class byBase : public Base // 子类 (派生类)
{
void func2()
{
cout<<"func2()"<<endl;
}
};

byBase 会继承 类 Base 的函数 func() 从而拥有两个成员函数 func()func2()

继承方式

1
protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。

使用共有继承方式,最大权限为公有

使用保护继承方式,最大权限为保护

使用私有继承方式,最大权限为私有

以私有方式继承,子类的子类什么都访问不到

继承的对象模型

不管怎样继承,父类中的非静态成员属性都会被子类继承下去。

每个子类都会有一份继承自父类的成员

构造和析构顺序

当创建子类对象时,先有父类对象还是子类对象?

先构造 父类 , 后构造 子类 。

析构的顺序和构造的顺序相反。

继承中同名成员的处理方式

在继承中遇到同名成员变量 需要在.后加父类的作用域

1
s.Base::a = 10; // 表示使用的是继承自父类的成员变量

对于成员函数 同理

1
s.Base::func();

如果子类中出现了和父类中同名的成员函数,那么子类会将父类中的所有成员函数隐藏掉,包括重载的函数
必须加作用域才可以访问。

继承中静态成员变量的处理方式

静态成员变量可以通过类名访问。

1
2
// 通过子类访问父类继承的静态成员变量
Son::Base::m_A;

C++多继承

C++允许一个类继承多个类

当父类中出现同名成员 需要加作用域区分

在开发中不建议采用多继承写法

菱形继承 (virtual)

两个派生类继承同一个基类,又被同一个派生类继承。

1
2
3
4
5
   A
/ \
C D
\ /
E

E通过C,D继承A的成员时,只需要一份,却继承了两份,造成资源浪费

此时可以通过虚继承实现

继承时加上virtual 后,继承方式变为虚继承 , 数据只存一份

并且虚继承默认访问的是第一个基类

虚继承的派生类中存的是vbptr 『虚基类指针』 指向 vbtable 『虚基类表』 记录了派生类成员相对基类成员的偏移量

3. 多态

静态多态和动态多态

静态多态:函数重载和运算符重载

动态多态:派生类和虚函数来实现动态多态

区别:

  • 静态多态的函数地址早绑定 => 在编译阶段就确定了函数的地址
  • 动态多态的函数地址晚绑定 => 程序运行时才能确定函数地址

C++允许父类的引用指向一个子类对象 不需要进行类型转换

通过在基类的函数前加入关键字virtual,可以实现地址晚绑定,使一个函数表现出不同的效果

动态多态的条件:

  1. 有基础关系
  2. 子类要重写父类的虚函数
1
2
重写:函数的返回值,形参和函数名都要相同
重载:根据形参的不同区分两个函数

动态多态的使用: 要用父类的指针或引用指向子类的对象 调用虚函数

多态的原理

有虚函数的基类中会存一个被称为vfptr的指针 即虚函数指针 (aka虚函数表指针)
它会指向一个虚函数表vftable 该表内部会记录一个虚函数的地址

子类会继承父类的vfptr指向自己的虚函数表 父类和子类不会共享一个虚函数表

类的对象的隐藏成员保存的类的虚函数指针(vfptr),指向类的虚函数表 虚函数表是一个顺序表 表中有许多槽(slot),每个槽中存放的是一个虚函数的指针(地址)

如果派生类重写了基类的虚函数,派生虚函数表将基类虚函数的地址替换为派生类虚函数的地址,
如果派生类中没有重写虚函数,派生虚函数表将保存基类虚函数的地址。如果派生类中还定义了虚函数,那么该虚函数的地址也会被添加到虚派生类表里去。

为了保证虚函数表的性能,C++会保证虚函数表的指针存在于对象实例的最前面的位置。

当父类的指针或者引用指向子类对象时,就会发生多态

在创建派生类的对象时,虚函数指针会被指向派生类虚函数表。

纯虚函数和抽象类

多态中父类的虚函数大多没什么用,可以将虚函数改为纯虚函数。

1
virtual int func(int a,int b) = 0;  // 定义一个纯虚函数 

只要类中有一个纯虚函数,类就会成为抽象类 无法实例化一个对象

抽象类的子类必须重写抽象类的纯虚函数,否则该子类也属于抽象类,无法实例化对象

虚析构和纯虚析构

多态使用时,父类的指针无法释放子类中的析构代码,所以子类数据无法释放,会造成内存泄漏。

此时需要利用虚析构纯虚析构 虚析构和纯虚析构都需要函数的实现

如果类中有了纯虚析构,该类也会属于抽象类

只要在父类的析构函数前加上virtual 即可调用子类的析构函数

纯虚析构也能有这个效果,但是纯虚析构必须在类外实现函数体

1
2
3
4
Animal::~Animal()
{
/* 实现 */
}

泛型和STL

C++支持泛型编程STL

泛型编程主要靠模板实现,可大大提高复用性。

模板

  1. 模板不可以直接使用,它只是一个框架。
  2. 模板的通用并不是万能的。

模板的语法

  • 函数模板
  • 类模板

函数模板

语法:

1
2
template<typename T>
// 函数声明或定义

写模板时可以先不指定返回值和形参是什么数据类型 使用的时候再确定数据类型

template: 声明创建模板
typename: 表明T是数据类型的名称 可以用class替代
T 是可以替换的数据类型 (自定义)

***36_STL


STL

STLStandard Template Library 标准模板库

STL 从广义上分为 容器(container) 算法(algorithm) 迭代器(iterator)

迭代器连接了容器与算法

STL中的所有类基本上都使用了模板类和模板函数

STL的六大组件 – 容器、算法、迭代器、仿函数、适配器、空间配置器

  1. 容器 是各种数据结构 用来存储数据
  2. 算法 各种常用的算法 (sort,find,copy,for_each)
  3. 迭代器 迭代器 交流容器和算法
  4. 仿函数 函数调用运算符的重载
  5. 适配器 用来修饰容器或者仿函数或迭代器接口
  6. 空间配置器 负责空间的配置和管理

容器分为序列式容器和关联式容器

序列式容器强调值的排序,每个元素都有固定的位置

关联式容器各元素之间没有严格的物理上的顺序关系

算法分为质变算法和非质变算法

质变算法在运行期间会更改元素的内容 (增,删,改)

非质变算法在运行期间不会改变元素的内容 (查找,计数)

算法要通过迭代器才能访问容器里的元素 每种容器都有自己专属的迭代器

迭代器非常类似于指针

迭代器的种类: 输入、输出、向前、双向、随机访问

输入迭代器对数据进行只读访问

输出迭代器对数据进行只写访问

前向迭代器可以向前推进,并且可以进行读写操作

**双向迭代器**可以进行读写操作,并且可以向前和向后操作

**随机访问迭代器**支持读写操作,并且可以以跳跃的方式访问任意数据

vector容器

vector容器可以理解为一个数组。

string容器

封装了C语言的字符串