C++学习笔记
前言
俗话说工欲善其事必先利其器,C/C++作为编码基础,还是需要多学习了解的
多态
什么是多态
顾名思义,多态就是指多种状态,即同一个方法的行为随上下文而异,当类之间存在层次关系,并且通过继承关联时,就会用到多态。在程序的实现上,通过父类指针调用父/子类的函数,进而实现父类指针的多种形态,简单说,就是在父类与子类中存在同一函数名,参数,返回值的函数,在进行调用时,会根据调用函数的对象的类型执行不同的函数。
多态主要分为两大类:动态多态与静态多态,其中静态多态又分为函数重载与泛型编程,而动态多态则是虚函数。
静态多态
函数重载实质上是一个简单的静态多态,在实际中用的比较多,就不做过多的解释,主要为:同函数名与返回值,根据不同的参数类型、参数数量实现不同的功能,在编译器编译期间完成,如果能找到合适的函数,就调用,如果找不到,就报错。
泛型编程,则是通过函数模板与类模板实现的,通过指定参数类型实现功能实现,其中,STL就是基于泛型编程思想的成果。
动态多态
重中之重,C++的主要特点之一,不一定要用,但一定要了解。与静态多态最主要的区别就是,在程序运行时根据基类指针指向的对象确定到底需要调用哪个虚函数。
现在是北京时间15:53,我在考虑晚饭是吃玉米还是牛排,就以晚餐吃什么举个例子:
#include <iostream>
#include <memory>
using namespace std;
class Food
{
public:
void EatCorn() {cout << "eat corn" << endl;}
void EatSteak(){cout << "eat steak" << endl;}
};
class Dinner
{
public:
virtual void EatDinner(Food& tb) = 0;
};
class Steak :public Dinner
{
public:
virtual void EatDinner(Food& tb)
{
tb.EatSteak();
}
};
class Corn :public Dinner
{
public:
virtual void EatDinner(Food& tb)
{
tb.EatCorn();
}
};
int main()
{
Food tb;
Dinner* a = new Corn;
a->EatDinner(tb);
Dinner* b = new Steak;
b->EatDinner(tb);
system("pause");
return 0;
}
这是一个简单的动态多态的例子,它是在程序运行时根据条件选择到底该调用哪一个函数。代码解读:同函数名,同参数,同返回值,这是基本条件之一。EatDinner()
是构成多态的函数,在本例中,参数没有作用,只是为了说明要有同参数。
Food
类只是作为一个辅助,用来打印选择结果Dinner
类是父类Corn
子类,在这里表示玉米类Steak
子类,在这里表示牛排类
从上面代码中可看出,父类Dinner
中的虚函数没有定义,而是直接=0
,其实也是可以定义的,类似例子中这样,叫纯虚函数
。
纯/虚函数
虚函数,为多态而生,在基类中用virtual声明,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态连接到该函数,而是在动态运行中,根据对象类型的不同而选用不用的函数,称之为动态链接。在虚函数的概念中,有一类特殊的虚函数,叫做纯虚函数,是为了让子类更好的定义(或者是说没有必要在基类中定义主体)
动态多态的条件
- 基类中必要包含虚函数,并且在派生类中对虚函数重写
- 通过基类对象的指针或引用调用虚函数
哪些函数不能作为虚函数
- 友元函数(friend),因为他不是类的成员函数
- 全局函数
- 静态成员函数,因为他没有this指针
多态缺陷
- 效率低:多态机制需要先查询虚表的地址,再查询对应的函数
- 空间浪费
基类析构函数为虚函数理由
目的:解决内存泄漏的问题
当我们用基类指针 ptr 指向派生类对象的时候,如果基类析构函数没有定义为虚函数,在用delete ptr析构的时候程序就只会调用基类的析构函数而不会调用派生类的析构函数。
而当定义为虚函数的时候,delete ptr的时候会调用派生类的析构函数,派生类析构完成之后会自动调用基类的析构函数。
使用心得
作为外行人,一大堆介绍只会一堆情况–不知道,所以,在这里记录下一些使用心得:
- 函数重载用的最多,其最大的作用就是简化开发流程,在实际运用中,为了实现同一个功能,可能会遇到输入不同参数的情况,此时,用函数重载较为方便,例如,要实现一个功能,但数据来源不用,实际应用是通过内存传递,但离线测试则需要从本地读取,因此,可利用重载的方式,如下所示,只有一个参数类型不同:
bool U_Grap_Lego(const char *filename, UTechRobotPose &p);
bool U_Grap_Lego(const std::vector<UTechPoint3Df> data, UTechRobotPose &p);
- 动态多态用的少点,从测试历程中总结,就是不同的类(基类与子类)各有一个虚函数(同名、同参、同返回),程序在执行时,不同的对象类型调用不同的虚函数。
智能指针
智能指针与普通指针最大的区别就是:可自动释放内存,不需要代码释放,如果使用new
,则需要free
,如果使用malloc
,则需要delete
,但智能指针的话,就可以只创建即可,在平时的工作中用的的不多,就简单的介绍下,智能指针主要有四个:auto_ptr
、unique_ptr
、shared_ptr
、weak_ptr
auto_ptr
C++98的产物,C++11已经放弃,理由很简单,就是不够安全,会造成潜在的内存崩溃问题。unique_ptr
shared_ptr
多个指针可指向相同对象,该对象及资源会在最后一个引用被销毁的时候释放;其缺点是:当两个对象互相使用一个shared_ptr成员变量指向对方时,会带来循环引用,造成引用计数失效,从而导致内存泄漏weak_ptr
协助shared_ptr工作,避免循环引用问题的出现
友元函数
友元函数,关键词为friend
,bug
一样的存在,C++
的类本就是为了封装
起到保密的作用,但友元函数/类的存在就是开了一个漏洞,让类外的函数可以访问私有成员与保护成员。
友元函数虽然是在类中声明,但它却不是成员函数,所以,他可以放在private
中,也可以在public
中,因此,一切与成员函数有关的特性都与他无关。
介绍下友元类的一些特点:
- 友元关系不能被继承
- 友元关系是单向的,不具有交换性:若类B是类A的友元,则类A不一定是类B的友元,要看在类中是否具有相应的声明
- 友元关系不具有传递性:若类B是类A的友元,类C是B的友元,则C不一定是A的友元,同样要看类中是否具有相应的声明;
疑问点是否可在所有的类中定义一个万能的友元类,达到可访问所有数据的功能
抽象类(C++)
最近看了些资料,发现抽象类在C++与Java中的实现方式是不一样的,在Java中需要用到关键词abstract
,而在C++中则没有要求。
含有纯虚函数的类成为抽象类,抽象类只能作为派生类的基类,不能定义对象,但可以定义指针。在派生类实现该纯虚函数后,定义抽象类对象的指针,并指向或引用子类对象,因为是用到纯虚函数实现的,所以具有以下特点:
- 定义纯虚函数时,不能定义虚函数的实现部分
- 在没有定义纯虚函数前,不能调用这种函数。
抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。派生类如果不能实现所有的纯虚函数,那么派生类也成了抽象类。
抽象类的特点: - 抽象类只能用作其他类的基类,不能建立抽象对象
- 抽象类不能用作参数类型,函数返回类型或显示转换的类型
- 可以定义指向抽象类的指针和引用,此指针可以指向他的派生类,进而实现多态性。
AB与BA概念区别
函数模板与模板函数
函数模板建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,形式:template
模板函数将类型形参实例化的参数称为模板实参,用模板实参实例化的函数称为模板函数
函数指针与指针函数
函数指针表示指向指针的函数,如int (*pf)()
指针函数表示返回指针的函数,如int *fun()
数组指针与指针数组
数组指针 重点在指针,表示一个指针,指向一个数组,如int (*pa)[8]
指针数组重点在数组,表示包含的元素是指针,如int *ap[8]
C/C++代码的编译过程
- 预处理:处理各种宏定义,交给编译器
- 编译:编译成*.s的汇编代码
- 汇编:汇编代码形成*.o的机器码
- 链接:将目标文件(库函数)链接成完整的可执行程序
C++面向对象的三个特点并简述
- 封装:把数据与实现过程包装起来,隐藏实现细节,使代码模块化
- 继承:从一般到特殊的过程,对原有类(父类)中的功能进行扩展。主要包括:实现继承(使用基类的属性与方法,无序额外编程)、接口继承(使用基类的属性与方法的名字,子类实现)、可视继承(子类使用基类的外观和实现代码的能力)
- 多态:多种形态,根据对象类型的不同调用不同的函数
C/C++中强制类型转换
- static_cast
- 用于基本类型间的转换
- 不能用于基本类型指针间的转换
- 用于有继承关系类对象间的转换和类指针间的转换
- dynamic_cast
- 用于有继承关系的类指针间的转换
- 用于有交叉关系的类指针间的转换
- 具有类型检查的功能
- 需要虚函数的支持
- reinterpret_cast
- 用于指针间的类型转换
- 用于整数和指针间的类型转换
- const_cast
- 用于去掉const属性
- 目标类型必须是指针或引用
指针与引用区别
(1) 引用必须被初始化,指针不必。
(2) 引用初始化以后不能被改变,指针可以改变所指的对象。
(3) 不存在指向空值的引用,但是存在指向空值的指针。
STL
- vector 内存管理形式
vector分配的是连续的内存,通常 vector中分配的内存一般比实际的内存要大,即 vector.size() <= vector.capcity()
vector在分配内存时,一般是*2分配的,这样,可以做到取放更省时间
关键函数释义:
resize
: 修改size
,当resize
的值大于size/capcity
时,则size/capcity
都变;若小于时,则只改变size
不改变capcity
;
reserver
:修改capcity
,但不一定有效,当vector
的现有实际size
大于reserve(n)
的n
值时,vector
不做任何修改;
push_back:构造临时变量,尾部添加,析构临时变量
emplace_back:直接尾部添加,效率更高,但是,代码可读性和容错性较差
Lambda 表达式
Lambda 表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传。
捕获列表:用来获取上下文中的数据,常见形式,[], [=], [&]
参数列表:函数的输入
函数体:函数的实现
可变规则: multable
和 const
异常类型:抛出异常
返回类型:可以省略,直接在 函数体中 return
即可
示例:
sort(arr.begin(), arr.end(), [&tmp](int a, int b) {
std::cout << "tmp = " << tmp << std::endl;
return a > b;
});
C++关键词
constexpr
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
修饰普通常量 简单讲就是把变量常量话,例如,
constexpr int num = 1 + 2 + 3;
int url[num] = {1,2,3,4,5,6};
若不加 constexpr
修饰,则会报错url[num] 定义中 num 不可用作常量
;
explict
指定构造函数或转换函数 (C++11起)为显式, 即它不能用于隐式转换和复制初始化,其目的是为了避免隐式转换的构造函数出现。