一:内联函数与宏定义的
宏定义:
#define 标识符 字符串
eg:
#define max(a,b) a>b ? a:b;
用法:在主程序中遇到max(a,b)时候会把a和b替换到后面的式子
优点:(1)省去函数调用等步骤,提高效率。函数调用要开辟一个栈空间,将返回地址、形参等压栈,函数返回的时候还要释放栈空间。这会带来很多的时间开销。(2)宏定义与参数无关,较灵活。而函数调用会规定形参的类型,只能传进特定实参。
缺点:宏展开会造成代码长度过长。带参数宏定义的时候,很多时候会引起歧义,eg cal(a,b) a*b,若函数中是max(a++,b),替换的时候会变成a++ > b ? a++:b,a自增了两次,所以带参数宏定义的时候比较讲究。
补充:程序编译的时候会经过几个步骤(1)预处理,做程序展开的和替换的操作,例如将.h文件展开到#include处,进行宏替换等工作,不做任何运算功能 (2)编译:将高级语言编译成汇编语言 ,将程序形成若干个目标模块。(3)链接。将目标模块和他们需要的库函数链接在一起,形成一个完整的装入模块。(4)装入。将最终模块装入内存中。
内联函数:
用法:
inline
eg inline int max(int a,int b){
a>b ? a:b
}
优点:
在调用内联函数的时候,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数 都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检 查,或者进行自动类型转换。
缺点:
内 联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,若每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。(若两次调用同一个函数,且函数的程序较长,那么多次用内联就会多次复制,不如只进行函数调用好)
二:C++三大特性:封装、继承,多态
(1)public 、protected、private三者区别:
若是public,则子类和类外的任意访问者都可以直接访问,代表数据是公开的,不加以保护的。
若是protected,则只有自己和子类才可以直接访问,对数据稍微加以保护
若是private:则只有自己才可以直接访问。
三:函数重载
定义:在同一个作用域内的几个函数名字相同,但是形参不同(形参的数量或者类型不同)。编译器会根据传递的形参类型推断想要的是哪个函数
作用:在一定程度上减轻程序员起名字、记名字的负担
注:和重写区别开来,重写是指父类中的某个函数前面加了virtual,变成虚函数。然后子类定义了一个函数名、类型、返回值都完全一致的函数。这就叫做重写。
四:类和对象
(1)在.h文件中定义类的方法
为了避免在main函数中定义多个类造成main函数可读性不强,可将类的定义放在.h头文件中,头文件名字要和类名一致。
eg:"sales_data.h"头文件中这样定义一个类
#ifndef SALES_DATA_H
#define SALES_DATA_H
class sales_data{
...
}
#endif
上面定义一共分为三部分:
①#ifndef 与#endif ,作用:主要是为了防止头文件的嵌套,避免多次重复预处理和编译。
②#define
③类的定义部分
(2)构造函数:
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的函数名和类名一样,且没有返回值。
默认构造函数:若类中没有显式地给出构造函数,则编译器会隐式地提供一个构造函数,叫做默认构造函数。该默认构造函数不接受任何实参
eg:
struct Sales_data{
Sales_data() = default; //默认构造函数,没有形参, = default可写可不写,写的话突出是构造函数
Sales_data(const string &s,unsigned n,double p) : bookNo(s),units_sold(n),revenue(p*n)
{ };
Sales_data(std::istream &);
}
Sales_data :: Sales_data(std::istream &is){
read(is,*this); //从is中读取一条信息存到this对象中
}
第二个构造函数中:和{}中间的部分叫做构造函数初始值列表,函数体是为空的,即只在初始值列表中初始化,函数不需要其他的操作,当然,也可以不需要初始值列表,而是在构造函数体内干活,如第三个构造函数。
(3)拷贝、析构
对象的拷贝相当于把数据成员复制过去
五:访问控制和封装
(1)使用struct和class定义类有什么不同:使用struct定义默认在访问说明符之前的成员都是public,而使用class则是private,仅仅是这一点不同,其他定义方式都是一样的。
(2)对象和类
面向对象和面向过程的区别:形象理解:蛋炒饭和盖浇饭
(1)面向对象是以功能来划分问题,而不是步骤。面向对象就是高度实物抽象化、面向过程就是自顶向下的编程!
(2)面向对象把数据和函数模块整合在一起,面向过程是分开。
具体可以参照这篇博文, https://blog.csdn.net/jerry11112/article/details/79027834
(3)怎么样理解封装性:
封装的目标是实现程序的高内聚,低耦合,把程序给模块化。而类的出现实现了这个功能。而且。类中还可以用访问控制符,使类中的属性只要调用类中的方法才可以使用,大大加强了类的封装性。
作用:①:被封装的类的具体实现细节可以改变,但是并不需要调整用户级别的代码。一旦数据成员被定义成private,类的作者就可以比较自由地去修改数据。换句话说,只要类的接口不变,用户代码就不会改变。使得代码更加容易维护(软件工程中的概念)②:把数据成员定义成private的,还可以避免用户对数据造成破坏。一旦数据出现问题,还方便我们去定位问题所在
(4)友元:如果某个类的数据被定义成private了,则不是类的成员函数,访问不了这个私有数据。要实现非成员函数也可以进行这个数据的访问,可以把非成员函数定义为这个类的友元。(注意是在原本那个类里面定义)
class Sales_data{
friend Sales_data add(const Sales_data&,const Sales_data&);
private:
double revenue = 0.0;
}
这里也要区分一下成员函数和接口函数,若一个函数是在类外定义的,则这个函数是接口函数,但是非成员函数。
类还能把其它类定义成友元,也可以把其他类的成员函数定义成友元
class Screen{
friend class Window_mgr;
}
则Window_mgr类可以访问类Screen的所有成员
class Screen{
friend void Window_mgr :: clear(ScreenIndex);
}
上面定义友元函数的时候并不是对这个函数的真正声明,下面要用的时候,必须对这个函数再次声明。也就是说友元声明的作用只是影响访问权限,并非普通意义上的定义。
六:泛型算法---在#include<algorithm>头文件中
定义:标准库并未给每个容易都定义成员函数来实现这些操作,而是定义了一组泛型算法。这组泛型算法实现了一些经典算法的公共接口,可以用于基于不同类型的元素和多种容器类型,是基于迭代器的。
(1)只读算法:
find(v.begin(),v.end(),val); ---在v中查找val,并返回val的位置
equal(v1.cbegin(),v1.cend(),v2.cbegin());--比较v1中的每个元素,在v2中是否都有可以对应相等的元素,若有,则返回true
(2)写算法:
fill(v.begin(),v.end(),0); ----重写v中元素为0
注意一下一个错误:
vector<int>vec;
fill_n(vec.begin,10,0);
原本想实现把vec的前十个元素重置为0,但是这里vec是空的,并没有十个元素,引发错误
replace(v.begin(),v.end(),0,42);---将v中为0的元素都替换成42
(3)重排算法
//排序且删除words里面的重复元素
vector<string>words;
sort(words.begin(),words.end());
//unique会返回去重后的序列的后面的指针
auto end_unique = unique(words.begin(),words.end());
words.erase(end_unique,words.end());
泛型算法只能对迭代器操作,不能对容器本身操作,所以要真正删除容器的元素时,应调用容器本身的函数
(4)lambda:可作为一些函数的参数,实现一些具体的功能
定义:
[] () ->returntype { };
[]里面是这个函数需要捕获局部变量、(这个局部变量在之前有定义)()是这个函数的形参,接下来是返回类型,函数体部分。
捕获与返回:
①值捕获:捕获一个值的拷贝
size_t v1 = 42;
auto f = [v1]{return v1};
v1 = 0;
auto j = f();
这里属于值捕获,得到的j还是42
②:引用捕获
size_t v1 = 42;
auto f = [&v1]{return v1;};
v1 = 0;
auto j = f();
得到的j还是0
七:继承与派生
(1)动态绑定:当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
eg:
class Quote{
public:
virtual double net_price(size_t n) const;
}
double print_double(const Quote &item,size_t n){
double ret = item.net.price(n);
return ret;
}
print_totle(basic,20);
print_totle(bulk,20);
basic 是基类对象,执行的是基类的函数,bulk是派生类对象,执行的是派生类的对象
(2)定义一个派生类:
class Bulk_quote : public Quote{
...
};
冒号后面加上访问说明符
(3)首先初始化基类的成员部分,然后按照声明的顺序依次初始化派生类的对象
(4)防止继承的发生
class Noderived final{ ... }; //Noderived 不能作为基类
class last final : Base{ ... }; //last是final继承,后面不能作为基类
(5)类型转换
派生类可以利用指针或者引用向基类转换,而基类不能像派生类转换
我们可以将一个派生类对象拷贝、移动或者赋值给一个基类对象,不过这种操作只处理派生类对象的基类部分
八:抽象基类
含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口
纯虚函数定义:double net_size(size_t) = 0;
不能创建抽象基类的对象
九:虚析构函数
当我们delete一个动态分配的对象的指针时,将执行析构函数。如果我们想将析构函数动态绑定,则需要在基类处设置一个虚析构函数(和前面的虚函数理解是一样的)
eg:
class Quote{
public:
virtual ~Quote() = default; //动态绑定析构函数
}
Quote * it = new Quote;
delete it; //调用Quote的析构函数
it = new Bulk_quote;
delete it; //调用Bulk_quote的析构函数