面向对象的三大特点:封装、继承、多态。
在面向对象编程中,类(也就是类型)的概念很重要。类的是对具体事物的抽象表达,定义类后就可以申明具体的实体变量,也就是对象。
类中包含数据和方法(可以理解为函数),方法用于处理数据。数据和方法都是类的成员。
c++申明和定义类用class
关键字:
class Test { // 申明一个名为Test的类型
public: // 访问权限修饰符,c++中有public、protected、private三种
Test(); // 构造方法
~Test(); // 析构方法
int set_t(int tmp) {
t = tmp;
};
int get_t() const {
return t;
}
protected: // 受保护成员
private: // 私有成员
int t; // 申明一个类变量
}; // 类定义后必须跟着一个分号或一个声明列表
类中两个特殊的方法是构造方法和析构方法。构造方法和类名同名,在对象被创建被调用。析构方法是在类前面加上~,在对象被销毁时调用,在析构函数中可以释放申请的堆内存。
有个问题,构造函数和析构函数必须定义为public嘛?
如果不写构造函数和析构函数,编译器会为其生成默认的无参构造函数和无参析构函数。
编译器会给一个类生成默认函数的,包括:
- 默认构造函数
- 默认析构函数
- 默认拷贝构造函数
- 默认赋值函数
- 移动构造函数
- 移动拷贝函数
class DataOnly {
public:
DataOnly () // default constructor
~DataOnly () // destructor
DataOnly (const DataOnly & rhs) // copy constructor
DataOnly & operator=(const DataOnly & rhs) // copy assignment operator
DataOnly (const DataOnly && rhs) // C++11, move constructor
DataOnly & operator=(DataOnly && rhs) // C++11, move assignment operator
};
在项目代码中通常会看到在定义的方法后面写上=delete
、= defaule
,在网上找了找博客学习,知道其中的含义如下:
=delete
的作用
- 禁止使用编译器默认生成的函数(类的构造函数、析构函数)
- delete 关键字可用于任何函数,不仅仅局限于类的成员函数,表示该函数被禁用
- 在模板特例化中,可以用delete来过滤一些特定的形参类型
=default
的作用
在程序员重载了自己上面提到的C++编译器默认生成的函数之后,C++编译器将不在生成这些函数的默认形式。
但是C++允许我们使用=default来要求编译器生成一个默认函数。
有篇很不错的博客推荐:C++中 =defaule 和 =delete 使用
**如果程序员重写了有参数的构造函数,编译器还会保留无参的默认构造函数嘛?答案是不会。**重写了有参构造,编译器会删掉默认无参构造。做个小实验:
#include
using namespace std;
class Base {
public:
Base(int i): i(i) {};
int i;
void print_i() {
cout << i << endl;
}
};
int main() {
Base b = Base();
b.print_i();
return 0;
}
// 编译时报错:error: no matching function for call to 'Base::Base()'
所用我们用=default
#include
using namespace std;
class Base {
public:
Base() = default;
Base(int i): i(i) {};
int i;
void print_i() {
cout << i << endl;
}
};
int main() {
Base b = Base();
b.print_i();
return 0;
}
// 成功输出i的值0
C++类成员的访问修饰符有public、protected、private三种,分别申明的是公有成员、私有成员、受保护成员。
继承允许我们依据另一个类来定义一个类,可以提高代码的复用率,可以少些重复的代码,提高执行效率。
两个比较重要的概念是基类(或者说是父类)、派生类(子类)。派生类继承基类,可以直接获得基类中的方法和变量。基类中定义共同的特性,派生类继承后丰富具体的特性。
c++支持多继承,可以从多个基类继承数据和函数。
类继承的声明形式是class derivedClassName: access-specifier BaseClassName ...
修饰符 access-specifier 是 public、protected 或 private 其中的一个,如果未使用访问修饰符 access-specifier,则默认为 private。
// 申明示意
class Shape;
class Base;
class Rectangle: public Shape;
class Triangle: public Shape, public Base; // 多继承
class Pentagon: Shape; // 和 class Pentagon: private Shape;相同
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过访问修饰符 access-specifier 来指定的。
几乎不使用 protected 或 private 继承,通常使用 public 继承。
如果未使用访问修饰符 access-specifier,则默认为 private。
参考链接:https://www.runoob.com/cplusplus/cpp-inheritance.html
以下几种派生类无法从基类中继承:
#include
using namespace std;
class Base {
public:
Base() {
cout << "This is Base constructor." << endl;
}
~Base() {
cout << "This is Base destructor." << endl;
cout << endl;
}
};
class Derived: public Base {
public:
Derived() {
cout << "This is Derived constructor." << endl;
}
~Derived() {
cout << "This is Derived destructor." << endl;
}
};
int main() {
cout << "----------" << endl;
Base b;
cout << "----------" << endl;
Derived d;
cout << "----------" << endl;
return 0;
}
/* 输出结果:
----------
This is Base constructor.
----------
This is Base constructor.
This is Derived constructor.
----------
This is Derived destructor.
This is Base destructor.
This is Base destructor.
*/
编译时多态:主要指函数的重载,包括运算符的重载、对重载函数的调用,在编译时根据实参确定调用那个函数
运行时多态:基类的指针可以指向派生类对象。但是基类指针只能访问派生类的成员变量,不能访问派生类的成员函数。为了解决该问题,让基类指针能够访问派生类的成员函数,C++增加虚函数(Virtual Function)
重载、重写、覆盖的区别
1、重载overload,编译中发生,根据函数参数类型和个数做区分。
2、重写overwrite,继承中没有用virtual修饰的在父类中存在同名的函数,这种是不能形成多态的。
3、覆盖override,继承中的用virtual修饰的虚函数。
虚函数数声明:在函数声明前增加virtual关键字。虚函数要申明为public的,如果是private的,派生类继承基类中的虚函数是无法访问到的,无法重写,编译会报错提示。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)
多态是面向对象编程的主要特征之一,C++虚函数的唯一用处就是构成多态。
多态的实现
#include
using namespace std;
class Base {
public:
virtual void foo() { cout << "Base::foo() is called" << endl;}
};
class Derived: public Base {
public:
virtual void foo() { cout << "Derived::foo() is called" << endl;} //基类声明的虚函数,在派生类中也是虚函数。在派生类这里可以不用virtual关键字。
};
int main() {
Base b;
Derived d;
Base * bptr;
bptr = &b;
bptr->foo();
bptr = &d;
bptr->foo();
return 0;
}
/* 输出
Base::foo() is called
Derived::foo() is called
*/
多态的用途
多态在小项目中鲜有有用武之地。对于具有复杂继承关系的大中型程序,多态可以增加其灵活性,让代码更具有表现力。如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱;而有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数。
纯虚函数pure virtual function
申明示例:
class CShape { // 申明了纯虚函数的类为抽象类。因此CShape不能被实例化。
public:
virtual void Show() = 0; // 类似这种是纯虚函数。
};
class CPoint2D: public CShape {
public:
void Show() {
cout << "Show() from CPint2D\n" << endl;
}
}
满足两个条件时使用纯虚函数:
- 当想在基类中抽象出一个方法,且该基类只能被继承,不能被实例化
- 这个方法必须在派生类derived class中被实现
虚析构函数:当一个类被设计用作基类时,其析构函数必须是虚的,如果不是,多态调用派生类时,派生类的析构函数被调用时其实执行的是基类的析构函数。析构函数甚至可以是纯虚的。
构造函数不能是虚的。
构造函数和析构函数中的虚函数调用:一个类的虚函数在它自己的构造函数和析构函数中被调用时,它就是普通函数,也就是虚函数在构造函数和析构函数中不再多态。
模板是C++泛型编程的基础。分为函数模板和类模板。
函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。
类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。
内联函数使用关键字inline
申明。
其目的是为了提高函数的执行效率,用关键字inline放在函数定义的前面可将函数定义为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开。从而消除定义函数的额外开销。
内联函数和宏的区别
内联函数放入头文件,内联函数必须与函数定义放在一起才能使函数称为内联,仅将内联放在函数声明前面不起作用。C++ inline函数是一种“用于实现的关键字”,inline可以不出现在函数的声明中。
定义在类声明之中的成员函数将自动地称为内联函数。
class A { // 类申明
public:
void Foo(int x, int y) {...} // Foo自动为内联的。
void Test();
void setA();
}
void A::Test() {
std::cout << "A::Test" << endl;
}
引用“&”
右值引用“&&”
左值和右值区别
左值代表一个在内存总占有确定位置的对象(有一个地址)。所以的左值不能是数组,函数或不完全类型都可以转换成右值。
右值代表不在内存中占有确定位置的表达式,表达式的临时结果没有内存空间。
struct Data {
Data(int i = 0, int j =0)
: i(i), j(j) {}
int i, j;
}
命名空间是ANSIC++引入的可以由用户命名的作用域,用来处理程序中 常见的同名冲突。
在 C语言中定义了3个层次的作用域,即文件(编译单元)、函数和复合语句。C++又引入了类作用域,类是出现在文件内的。在不同的作用域中可以定义相同名字的变量,互不于扰,系统能够区别它们。
全局变量的作用域是整个程序,在同一作用域中不应有两个或多个同名的实体(enuty),包括变量、函数和类等。
使用命名空间解决名字冲突。
namespace {
}
有遇到使用using以双冒号::开头。比如using ::T::tt
::
开头代表全局访问的意思。
Global Names A name of an object, function, or enumerator is global if it is introduced outside any function or class or prefixed by the global unary scope operator (:, and if it is not used in conjunction with any of these binary operators: Scope-resolution (: Member-selection for objects and references (.) Member-selection for pointers (–>)
using ::testing::Invoke;
表示全局的testing。
using testing::Invoke;
表示在using这个语句所在作用域中的testing。
参考:https://bbs.csdn.net/topics/390841431
学习链接:https://www.runoob.com/w3cnote/cpp-static-library-and-dynamic-library.html
链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(****.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
Window与Linux执行文件格式不同,在创建动态库的时候有一些差异。
在Windows系统下的执行文件格式是PE格式,动态库需要一个**DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)**关键字。
Linux下gcc编译的执行文件默认是ELF格式,**不需要初始化入口,亦不需要函数做特别的声明,**编写比较方便。
与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。
二者的不同点在于代码被载入的时刻不同。
动态库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。带来好处的同时,也会有问题!如经典的DLL Hell问题,如何规避动态库管理问题??
void _max(T, T) const;
const不仅可以修饰变量为常量,还可以修饰函数的参数、返回值、甚至是函数体。
函数前面的const:返回值为const;函数后面的const:const函数。C++在函数声明时,后面跟个const是限定函数类型为常成员函数, 常成员函数是指不能改变成员变量值的函数,如果有则会在编译阶段就报错。表示成员函数是只读的,不能有写操作。
常成员函数需要在类中使用,不能在单独的函数中使用,否则报错。
const的函数不能对其数据成员进行修改操作int a2() const { return _a; }
。
const的对象,不能引用非const的成员函数,如const A a; a._a();是会报错
。
参考博客:https://blog.csdn.net/mid_Faker/article/details/104144826
void gain(T const&)
函数模版是运行时多态?类模板是编译时多态?
类的多态是运行时多态?
处理头文件包含(#include)、宏定义(#define)、条件编译(#if #else #ifdef #ifndef #elif #endif)、去掉注释
static_cast、inter_cast、
互斥锁、条件锁、读写锁