C++面向对象学习
面向对象的三大特点:封装、继承、多态。
C++类
类的申明和定义
在面向对象编程中,类(也就是类型)的概念很重要。类的是对具体事物的抽象表达,定义类后就可以申明具体的实体变量,也就是对象。
类中包含数据和方法(可以理解为函数),方法用于处理数据。数据和方法都是类的成员。
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 <iostream>
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 <iostream>
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三种,分别申明的是公有成员、私有成员、受保护成员。
- public成员:在类的外部和内部都可以被访问到。
- private成员:private变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类内部和友元函数可以访问私有成员,不能被派生类访问。
- protected成员:protected变量或函数与私有成员十分相似,但有一点不同,protected成员在派生类(即子类)中是可访问的。
- 如果不写,默认为private的。
在实际项目中,通常把类中的变量申明为private的,然后提供public的设置变量和获取变量的方法。
C++继承
继承允许我们依据另一个类来定义一个类,可以提高代码的复用率,可以少些重复的代码,提高执行效率。
两个比较重要的概念是基类(或者说是父类)、派生类(子类)。派生类继承基类,可以直接获得基类中的方法和变量。基类中定义共同的特性,派生类继承后丰富具体的特性。
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。
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
参考链接:https://www.runoob.com/cplusplus/cpp-inheritance.html
继承中的构造函数和析构函数
以下几种派生类无法从基类中继承:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
创建派生类时,先调用基类的构造函数,再调用派生类的构造函数。
销毁派生类始,先调用派生类的析构函数,再调用基类的析构函数。
做个小实验:
#include <iostream>
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++多态
C++中多态的分类
-
编译时多态:主要指函数的重载,包括运算符的重载、对重载函数的调用,在编译时根据实参确定调用那个函数
-
运行时多态:基类的指针可以指向派生类对象。但是基类指针只能访问派生类的成员变量,不能访问派生类的成员函数。为了解决该问题,让基类指针能够访问派生类的成员函数,C++增加虚函数(Virtual Function)
重载、重写、覆盖的区别
1、重载overload,编译中发生,根据函数参数类型和个数做区分。
2、重写overwrite,继承中没有用virtual修饰的在父类中存在同名的函数,这种是不能形成多态的。
3、覆盖override,继承中的用virtual修饰的虚函数。
虚函数的作用
-
虚函数数声明:在函数声明前增加virtual关键字。虚函数要申明为public的,如果是private的,派生类继承基类中的虚函数是无法访问到的,无法重写,编译会报错提示。
-
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)
-
多态是面向对象编程的主要特征之一,C++虚函数的唯一用处就是构成多态。
-
多态的实现
- 指针实现:通常指针调用普通的成员函数时会根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数。但是这种说法并不适用于虚函数,虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。
- 引用实现:引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针
#include <iostream>
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++知识点
C++模板
模板是C++泛型编程的基础。分为函数模板和类模板。
-
函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。
-
类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。
C++内联函数
-
内联函数使用关键字
inline
申明。 -
其目的是为了提高函数的执行效率,用关键字inline放在函数定义的前面可将函数定义为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开。从而消除定义函数的额外开销。
-
内联函数和宏的区别
- 宏代码本身不是函数。使用宏最大的缺点是容易出错,预处理器在拷贝宏代码时常常产生想不到的边际效应,比如优先级引起的错误。
- 宏是不可调试的,但内联函数可以调试。内联函数,在程序debug版本中根本没有内联,在发行Release版本中,程序才会真正内联,有的编辑器可以设置内联开关。
- 宏的另一个缺点是,不能操作类的私有成员。
-
内联函数放入头文件,内联函数必须与函数定义放在一起才能使函数称为内联,仅将内联放在函数声明前面不起作用。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;
}
- 不同于其他函数,内联函数应该在头文件中定义。
关于引用
-
引用“&”
- &的三种用法
- 按位与
- 取地址
- 引用
- 引用与指针的区别
- &的三种用法
-
右值引用“&&”
- &&的两种用法
- “与”逻辑运算符
- 右值引用
- 右值是程序运行中的临时结果,对右值的引用,可以避免赋值提高效率。
- &&的两种用法
-
左值和右值区别
左值代表一个在内存总占有确定位置的对象(有一个地址)。所以的左值不能是数组,函数或不完全类型都可以转换成右值。
右值代表不在内存中占有确定位置的表达式,表达式的临时结果没有内存空间。
c++的结构体
struct Data {
Data(int i = 0, int j =0)
: i(i), j(j) {}
int i, j;
}
c++命名空间的作用
命名空间是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问题,如何规避动态库管理问题??
其他
1. 为什么函数的定义后不加分号,类型的定义后面要加分号?
2. 函数名后面加const是什么意思?如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
3. 变量名后面加const&是什么意思?void gain(T const&)
4. 运行时多态和编译时多态
函数模版是运行时多态?类模板是编译时多态?
类的多态是运行时多态?
5. 头文件和源文件的区别,必须要有和源文件同名的头文件吗?
6. 为什么要存在虚拟内存呢?
7. 预编译的作用,那三个:
处理头文件包含(#include)、宏定义(#define)、条件编译(#if #else #ifdef #ifndef #elif #endif)、去掉注释
8. c++11后,const和constexpr的区别?
9. 四个智能指针
10. 四种强制类型转换及其区别?
static_cast、inter_cast、
11. C++多线程的锁有几种
互斥锁、条件锁、读写锁