保留字
const
欲阻止一个变量被改变,可以使用const关键字
在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
被const 修饰的变量,只能调用其自身的const函数,不能调用非const函数(也正是通过该特点,实现const对函数的重载)
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量(const实现对函数的重载);
对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为左值。
1
2
3
4
5const classA operator*(const classA& a1, const classA& a2);
// operator* 的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错
classA a, b, c;
(a * b) = c; // 对a*b的结果赋值
// 操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。
位置汇总
1
2
3
4
5
6
7
8 const char * const p;
// 第一个const表明不能通过p修改内容;
// 第二个const表明不能修改p
const int& func(const int& para) const{}
// 第一个const表明函数返回的引用所指向的内容不能被修改(以使其返回的值不为左值);例如,func(a) = 2; 在有const的情况下是非法的
// 第二个const表明传入的形参不能被修改;
// 第三个const表明函数是否是const的,const的函数只能被const的对象调用,non-const的函数只能被non-const的对象调用
static
区分自动变量还是静态持续无链接性变量
- 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
区分变量或者函数的作用范围
- 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
- 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
在类中修饰变量和函数
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;(和static修饰函数体内的变量的本质一样),可参考下文中的类中的变量
静态非常量数据成员,只能在类外定义和初始化,在类内仅是声明而已。 why?
- 静态常量数据成员可以在类内初始化(即类内声明的同时初始化)或者类外初始化,不可以在构造函数或者初始化列表中初始化。
- 静态非常量数据成员只能在类外初始化。因为所有的对象只有一份该成员,所以新建的对象不应该重新对其初始化,因此初始化不能放在类内。
- 非静态的常量数据成员不能在类内初始化,也不能在构造函数中初始化,而只能且必须在构造函数的初始化列表中初始化;
- 非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化;
从初始化位置的角度:
- 可以在类内中初始化的有const、static
- 可以在类外中初始化的有static
- 可以在构造函数中初始化的有non-const、non-static
- 可以在初始化列表中初始化的有non-static
TODO 静态常量数据成员的存储范围呢?
在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量;
变量
变量的描述
具体参考 C语言内存管理和指针交流研讨。
内存六元组模型
M = {Address, Variable_Type, Name, Size, Value, Value_Type}
M: 申明变量系统给分配的一段内存,粗略可以分为两部分:物理属性和逻辑属性,其中只有Address和Size是必不可少的,见malloc
的例子。
- 物理属性,机器知道的内容
- Address: 在内存中的第一个字节的地址,由系统分配,一旦确定无法修改
- Variable_Type:变量类型
- Name:变量名称
- Size:内存大小,必须要,且不可修改
- 逻辑属性,程序员知道的内容
- Value:内存表示的值
- Value_Type:表示值的类型
- 非数组类型,Value_Type=Variable_Type, Value是通过Value_Type去观察这段内存获得的值
- 数组类型,指向数组里元素变量类型的指针类型,Value是数组第一个元素所处内存的第一个字节编号(等于Address)
此处数组有如下比较重要的特性,定义数组 int arr [12]; int *p:
数组
*(arr+n)
和arr[n]
以及*(p+n)
都是相同的含义数组指向的是数组中的第一个元素,但是对数组取址,返回的是指向整个数组的地址;因此
arr+1
和&arr+1
有本质的区别
arr+1
,是在arr地址上加了一个int大小的距离
&arr+1
,是在arr地址上加了一个数组的大小的距离arr代表的地址和&arr代表的地址在数值上相等,在含义类型上不相等。
对数组进行
sizeof
计算,会得到整个数据部分的空间,即单个元素的大小乘以整个数组的长度指针指向的内存空间是通过
malloc
和free
分配与释放的;而数组是隐式的分配和删除区分指针数组和数组指针
1
2 int* arr[12]; // int指针的数组
int (*arr) [12] // 一个指向数组的指针
其他保留字
typedef 声明别名
typename 指定别名是类型
变量的存储位置
C++的内存分区
了解变量的存储位置前,需要先了解一下C++的内存分区,主要分为:栈、堆、全局/静态存储区、常量存储区、代码区(这些区域也是每个进程控制的区域)。
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
映射区:存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束编译器自动释放。
2021-10-14:具体理解存放的应该是静态常量,const static类型的变量
在实际实验过程中,baseType的常量地址小于静态变量的地址;但是string没有体现地址小于静态变量的地址
代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
说明:从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 栈 --> env
拓展了解。在一个Linux的进程的虚拟内存可以表示如下,
其中的
内核虚拟内存
包含内核中的代码和数据结构,其某些区域被映射到所有进程共享的物理页面,其他区域包含每个进程都不相同的数据。比如说,页表、内核在进程的上下文执行代码时使用的内核栈,以及记录虚拟地址空间当前组织的各种数据结构。有趣的是,Linux 也将一组连续的虚拟页面(大小等同于系统中 DRAM 的总量)映射到相应的一组连续的物理页面。这就为内核提供了一种便利的方法来访问物理内存中任何特定的位置。
存储位置
通过上述的介绍,变量可以位于data,heap和stack三类空间上。
Data
data区域泛指在.data
和.bss
的区域,比较大的特点是跟随代码块在一起,预先分配了存储空间。可以通过如下的方式在Data上创建变量:
- 在代码块外定义的变量
- 在代码块内定义的静态(static)变量
Heap
Heap是程序员自己管理的变量空间区域,可以通过如下的方式在堆上创建变量:
malloc
库函数,在堆上分配一个空间;与之对应的通过free
库函数进行释放new
操作符(operator),在调用了malloc
之后,还会调用构造函数,把变量初始化出来;与之对应的通过delete
函数进行析构并释放空间
malloc
和new
在返回结果上的差异
malloc
和new
返回的都是一个指针,malloc
返回的是一个void的指针,而new返回的一个指定对象的指针若没有分配到空间,
new
会有一个bad_malloc
的异常,而malloc
会返回一个nullptr
在
new [] BaseClass
的时候,会一次行分配所有的空间,再逐个初始化。在返回指针的指向的时候会有如下差异
1
2
3
4
5
6 | Bookkeeping | First Object | Second Object |....
^ ^
| |
| p2: This is what is returned by new[]
|
p1:this is what is returned by malloc()因此new [] 的返回结果需要使用delete []释放,如果用
delete
会出现pointer being freed was not allocated
的错误。
Stack
Stack是系统自动分配的,不受程序员的控制,存放的也都是局部变量,可以通过如下的方式在Stack上创建变量
- 代码块内的非静态(non-static)变量
变量的生命周期
变量的声明周期和变量的存储位置紧密相关。具体可以分为如下几大类:
静态
此类变量全部分配在Data区域,包括全局(非静态或静态)变量、局部静态变量。
全局(非静态或静态)变量在main函数运行前分配空间并初始化(默认初始化为0);在main函数运行后消亡
此处的非静态和静态指对象的可见范围是文件内还是文件间,和生命周期并不相关
局部静态变量在main函数运行前分配空间,在第一次被调用时初始化;在main函数运行后析构;在C++11之后,针对局部静态变量的创建还提供线程安全的保障
提前分配空间是根据data段大小固定判断出来的
自动
作用域可以分为全局作用域、局部作用域、语句做用域、类作用域、命名空间作用域和文件作用域;(不是很理解
此类变量全部分配在Stack上,会在变量定义的时候创建,在作用域结束时消亡。主要指局部变量(自动变量)。
由程序员指定
这类变量主要在堆上,通过程序员手动创建和释放。
变量的可见范围
变量的可见范围主要针对伴随程序生命周期的变量进行描述的(其他生命周期中的变量都是在当前函数或者代码块中可见的),可以分为如下:
无链接性
局部变量,只能在当前函数或者代码块中访问。
内部链接性
全局静态变量,只能在当前文件中访问。
外部链接性
全局变量,在其他文件中也可以访问
变量的可修改性
是否被const修饰,
变量的数据类型
基本类型
复合类型
用户自定义类型
类中的变量
参考下面的例子
1 | class A { |
指针
指针和引用的区别
智能指针
auto_ptr
所有权进行传递,例如p2=p1
之后,p1实际上已经失效了。再调用p1
将会报错,因此auto_ptr
存在潜在的内存崩溃问题。
unique_ptr
所有权严格独占的模式,p2=p1
的语句会报错(除非p1是一个临时右值);相比于auto_ptr
更加的安全了。
- 初始化方式和shared_ptr的初始化方式类似通过构造函数或者
make_unique
函数,参见shared_ptr相关信息 - 自定义deleter函数和shared_ptr不一致,使用了模板的方式进行实现
1 | class Base { |
shared_ptr
共享式拥有的概念,多个指针可以指向同一个资源,该资源会在最后一个指针被析构的时候释放
shared_ptr的所有成员函数都是线程安全的,如果多线程同时访问同一个
non-const
的shared_ptr,那有可能发生资源竞争(Data Race)的情况,比如改变这个shared_ptr的指向,这种情况下需要多线程同步机制。其内部包含两个指针,一个指向共享对象,另一指向共享计数区域。
通过构造函数进行初始化
若在构造shared_ptr的时候传入的是空指针,那么引用计数会是0
在构造时只能传入指向heap的指针,而不能是一个栈的地址
不允许通过一直原始指针初始化多个shared_ptr,会引起多次delete操作
允许自定义删除行为
1
2
3
4
5
6int *p = new int[10];
auto func = [](int *p) {
delete[] p;
cout << "Delete memory at " << p << endl;
};
shared_ptr<int> ptr(p, func);用途:删除数组,若指针指向的是一个数组,需要自定义删除(
deleter
)数组的行为(也可以采用C++17之后的标准支持)1
2
3
4
5
6
7
8
9
10
11
12class Basic {
public:
Basic() { cout << "Basic" << endl; }
~Basic() { cout << "~Basic" << endl; }
};
int main() {
Basic *p = new Basic[3];
shared_ptr<Basic> ptr(p, [](Basic *p){ delete[] p; }); // 自定义删除行为
// 或者
Basic *p2 = new Basic[3];
shared_ptr<Base[]> ptr(p2);
}
通过
make_shared
初始化接收的参数可以是一个对象,也可以是一个跟该类的构造函数匹配的参数列表
1
2auto ptr1 = make_shared<vector<int>>(10, -1);
auto ptr2 = make_shared<vector<int>>(vector<int>(10, -1));与通过构造函数初始化不同,
make_shared
允许传入一个临时对象,会将该对象拷贝到heap中和原来的对象的存储空间分离开;此外通过指针修改内容不会影响到原来的对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
auto ptr = make_shared<vector<int>>(v);
// &v = 0x7fffffffd9c0
// ptr.get() = 0x55555556dee0
cout << &v << endl;
cout << ptr.get() << endl;
// v[0] is still 1
(*ptr)[0] = 100;
cout << v[0] << endl;
return 0;
}
指向一个函数
1
2
3
4
5void func() { cout << "hello" << endl; }
int main() {
shared_ptr<void()> ptr(func, [](void (*)()) {}); // 必须自定义deleter的行为
(*ptr)();
}函数名称 含义 make_shared 传入构造函数 operator = 赋值函数 use_count 返回引用计数的个数 unique 返回是否是独占所有权( use_count 为 1) swap 交换两个 shared_ptr 对象(即交换所拥有的对象) reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1));
sp
与sp.get()
是等价的
当两个shared_ptr相互指向的时候会出现循环计数的问题,从而造成内存泄漏,可以采用weak_ptr
协助解决
weak_ptr
不控制对象寿命周期的智能指针
weak_ptr
指针通常不单独使用(因为没有实际用处),只能和shared_ptr
类型指针搭配使用。不会改变引用计数的数量
weak_ptr
没有重载*
和->
运算符,因此weak_ptr
只能访问所指的堆内存,而无法修改它成员函数如下:
函数 作用 operator = weak_ptr
可以直接被weak_ptr
或者shared_ptr
类型指针赋值swap 与另外一个 weak_ptr
交换 own objetcreset 置为 nullptr
use_count 查看与 weak_ptr
指向相同对象的shared_ptr
的数量expired 判断当前 weak_ptr
是否失效(指针为空,或者指向的堆内存已经被释放)lock 如果 weak_ptr
失效,则该函数会返回一个空的shared_ptr
指针;反之,该函数返回一个和当前weak_ptr
指向相同的shared_ptr
指针。weak_ptr会指向shared_ptr的数据,通过lock来获取数据,会随着shared_ptr的资源销毁而失效。
实现的一种思路:代码链接
demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
// global weak ptr
weak_ptr<int> gw;
void observe() {
cout << "use count = " << gw.use_count() << ": ";
if (auto spt = gw.lock()) cout << *spt << "\n";
else cout << "gw is expired\n";
}
int main() {
{
auto sp = make_shared<int>(233);
gw = sp;
observe(); // use count = 1: 233
}
observe(); // use count = 0: gw is expired
}weak_ptr解决循环引用造成的内存泄漏的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Parent;
class Child;
class Parent {
public:
shared_ptr<Child> childptr; // 换成weak_ptr<Child> childptr; 之后会出现析构才能正常
Parent() { cout << "Parent" << endl; }
~Parent() { cout << "~Parent" << endl; }
};
class Child {
public:
shared_ptr<Parent> parentptr; // 换成weak_ptr<Child> childptr; 之后会出现析构才能正常
Child() { cout << "Child" << endl; }
~Child() { cout << "~Child" << endl; }
};
int main() {
shared_ptr<Parent> parent(new Parent());
shared_ptr<Child> child(new Child());
parent->childptr = child;
child->parentptr = parent;
} // 没有产生析构的输出;
// 两处其实只需要一处修改成weak_ptr,打破环路即可
// parent child
// | |
// V V
// +---Parent---+ +---Child---+
// | childptr |---------->| |
// | |<----------| parentptr |
// +------------+ +-----------+
// 在堆上的指针不要是shared_ptr,因为存在于堆上,没有显示的delete操作会造成相互引用,谁都不能计数为0
类
c++提出了类的概念这也是c++面向对象编程的重要体现。面向对象的三大特征是
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或友元函数访问
继承
- 基类(父类)——> 派生类(子类);继承包括接口继承和实现继承。
多态
多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。
- C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载。是静态多态
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板。是静态多态
- 子类型多态(Subtype Polymorphism,运行期):虚函数。是动态多态
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
- C++ 多态分类及实现:
类函数
在c++11中提出了default
和delete
关键字,用于控制编译器自动生成函数的行为。
构造函数
成员初始化列表
成员的初始化列表,在下面4中情况必须要使用。成员初始化列表不指定的变量,在进入构造函数之前会调用他们的默认构造函数,初始化空间。指定的会使用指定的方法进行初始化。变量的初始化顺序由变量在类中声明的顺序决定的。
- 初始化一个reference member(本质上是一个const的指针)
- 初始化一个const member
- 调用一个base class的constructor,而他拥有一组参数
- 调用一个member class的constructor, 而他拥有一组参数
默认构造函数
对于一个class,若没有任何 user-declared constructor,那么会有一个default constructor被implicitly声明出来,这个声明通常是trivial(无能的,在STL中会采用直接内存操作的方式初始化)的,但在以下四种情况,是non-trivial的。注意这里编译器提供的构造函数仅为了编译器的需要,并不对用户负责。
- 带有Default Constructor的Member Class Object时,编译器声明的constructor会调用member class object的default constructor。这种情况下,即使有user-declared constructor,编译器也会按照Member Class Object定义的顺序补充调用他们的默认构造函数。
- 带有Default Constructor的Base Class,derived class A需要合成一个constructor来调用父类的constructor,因此也是non-trival的,如此之后再继承该class A的子类,将把合成的A的constructor当作是显示声明的。
- 此外即使A有user-declared的constructor,编译器也会扩充他们的每一个,按照顺序调用父类的default constructor。
- 和第一种情况类似,Member Class Object的default constructor也会被扩充进来
- 带有或者继承virtual function的class,因为需要给对象赋virtual function的地址;
- 带有一个virtual base class的class,因为需要给对象赋virtual class对象的地址。
拷贝构造函数
有三种情况,会以一个object的内容作为另一个class object的初值,如下
1 | class X {...}; |
拷贝构造函数和移动赋值函数之间的区别是
拷贝构造函数使用的时候是对象在初始化(定义)的时候
移动赋值函数使用的时候是对象已经完成初始化之后。
Copy Constructor的逻辑是把每一个内建的或者派生的data member(例如一个指针或者数组)的值,从一个objec拷贝到另一个object中;对于其中的member class object,会采用递归的方式进行memberwise initialization。
若没有显示的Copy Constructor声明,就会有Implicitly Copy Constructor被声明或者定义出来,他们是不是nontrivial的,会根据他们是否具有bitwise copy semiotics来判断。不遵照bitwise copy semiotics的情况
class中member objects中有声明copy constructor的情况(显式声明或者被编译器合成)
class继承自一个base class而后者存在一个copy constructor时
当class 声明了一个或者多个virtual functions时,由于可能会存在类型的在继承树上的变化,所以需要重新设定virtual table的指针
例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
}
class Bear : public ZooAnimal{
public:
Bear();
void animate();
void draw();
virtual void dance();
}
Bear beer;
ZooAnimal a = beer; // 会发生切割行为,逐位复制的方式将会造成vptr错误
当class 派生自一个继承串链,其中有一个或多个virtual base classes时
移动构造函数
移动构造函数通常在move函数的显式操作下执行,例如下面,c5
中持有的资源将会被转移到c6
中。
1 | Human c6 = std::move(c5); // 移动复制构造函数 |
析构函数
析构函数在变量被释放空间之前执行,用于处理一些善后的操作,例如
- 释放堆上的内存
- 释放持有的资源(例如锁)
- 。。。
应用场景:
- 正常的资源释放
- 锁的自动释放
赋值函数
和拷贝构造或者移动构造之间的区别是:赋值函数的执行需要变量已经被定义了;而构造函数调用的时候变量不能被定义。
- 拷贝赋值函数
- 移动赋值函数
在实际测试过程中发现,如果没有定义移动赋值函数,会使用拷贝赋值函数来代替。
友元函数
重载运算符
不能重载的运算符只有5个:
.
成员访问运算符.*
成员指针访问运算符::
域运算符sizeof
长度运算符?:
条件运算符
对于没有不带参数的构造函数,在定义变量的时候也不应该加括号;加了括号,反而不是一个变量的定义。
类的大小
对齐原则
与非静态成员变量(nonstatic data members)有关
虚函数
虚继承
虚继承中如果出现钻石继承,基类只会有一个存在,此外虚拟派生类中会有一个指向基类对象的指针,这些特性都会造成类大小的计算的差异
1
2
3
4
5
6class A{};
class B: virtual public A{};
class C: virtual public A{};
class D: public B, public C{
D():A(),B(),C(){};
}; //空类
假设结构体起始位置0,字节对齐的原则:首先找最长成员 a bytes,pack参数为 b bytes,那么有效单位长度为x=min(a, b)
,然后把地址按照 x 划分,然后一个个往里面填,最后不够 x ,要补足x。
类Demo
1 |
|
类继承
Inheritance可以让我们从base class中创造出来一个derived class,他们之间的关系是is-a
的关系
继承访问控制
Cpp可以实现如下的继承
1 | class Base { |
Cpp的Derived Class可以使用public、protected和private三种模式来进行继承。
继承模式 | 特点 |
---|---|
public inheritance | Base class中的public修饰的成员保持public,protected修饰的成员保持protected |
protected inheritance | Base class中的public和protected修饰的成员都变成protected |
private inheritance | Base class中public和protected修饰的成员都变成private |
Note:在Base class中使用private修饰的成员在Derived class中不可访问
虚继承
除了在继承访问控制中介绍的三种继承之外,还有一种重要的继承是虚继承;虚继承的一大特点是derived class不是在base class之后拼接,而是通过一个指针指向公共的父类。
虚函数表
动态联编通过每个类new
出来的虚拟指针vptr
指向一个该类所有需要动态联编函数的具体地址;
函数重载、重写、重定义
- 重载 (Overload) :函数名相同,参数列表不同,返回值没有要求(压根就不是一个函数)
- 重写 (Override) : 类
A
定义了一个虚函数func
,其子类B
把函数func
重新实现(aka, 覆盖)。可以修改函数的访问修饰符- 重定义 (Refine) : 类
A
定义了一个普通函数func
,其子类B
把函数func
重新实现个人理解函数定位依靠命名空间、函数名称、参数列表、是否const。
重写和重定义之间的区别就是函数是否是动态绑定的
重写的函数一定是virtual并且不是static的(static是类的函数,不是对象函数,不存在多态一说)
重载
根据函数标识,重载的函数压根就不是一个函数,没啥好说的。但是在c语言中是没有这个机制的,C语言对函数的标识仅局限于函数的名称(TODO 待验证)。
重写
是C++动态多态的重要实现方式,在c++11中提出了新的关键字来增强重写的规范性
final
:声明该虚函数不应该在被子类继承了override
:显示的声明这是一个重写函数,防止重写函数出现一些细节错误
使用Demo
重定义
如果相同的function在base class和derived class中都被定义了,那么一个dervied class的instance会执行derived class中的函数逻辑。但是也可以在下面的情况中执行base class的方法
Access Overridden Function to the Base Class
通过变量.BaseClass.function的方式执行base class的function。
Call Overridden Function From Derived Class
在Derived Class中调用Base Class的同名方法
Call Overridden Function Using Pointer
通过父指针指向一个Derived Class的instance,来调用function,会调用到的Base Class的function。
按照我的理解,C++读取instance需要按照某种Class来读取,当使用了Base Class的指针来调用instance的function,则会按照Base Class中的函数名对应的具体逻辑代码来执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// C++ program to access overridden function using pointer
// of Base type that points to an object of Derived class
using namespace std;
class Base {
public:
void print() {
cout << "Base Function" << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << "Derived Function" << endl;
}
};
int main() {
Derived derived1;
// pointer of Base type that points to derived1
Base* ptr = &derived1;
// call function of Base class using ptr
ptr->print();
return 0;
}
Output: Base Function
模板
类模板
specialization members和partial specializations参考原文参考
specialization members
对某写特定类型定制
1 |
|
partial specializations
对类型的不同形式(指针,引用...)定制
1 |
|
Traits
通过模板进行类型萃取(STL中的重要特性)如下代码链接
1 |
|
模板类
在编译时期产生,会生成具体的类,可以理解是类模板实例化后的一个产物
访问修饰符
Cpp中的访问修饰符有public、 private、 protected。
修饰符 | 访问 |
---|---|
private | 可以在class内或者friend classes and functions访问 |
protected | 在private的基础上,还可以被派生的类访问 |
public | 可以被所有的class和function访问 |
Note: class中的成员,默认是private的;在struct中的默认是public。
编译执行
预处理 -> 编译 -> 汇编 -> 链接
预处理
写好的⾼级语⾔的程序⽂本⽐如 hello.c
,预处理器根据#
开头的命令,修改原始的程序,如#include<iostream>
将把系统中的头⽂件插⼊到程序⽂本中,通常是以.i
结尾的⽂件。
编译阶段
编译器将 hello.i
⽂件翻译成⽂本⽂件 hello.s
,这个是汇编语⾔程序。⾼级语⾔是源程序。所以注意概念之间的区别。汇编语⾔程序是⼲嘛的?每条语句都以标准的⽂本格式确切描述⼀条低级机器语⾔指令。不同的⾼级语⾔翻译的汇编语⾔相同。编译策略可以分为静态编译和动态编译:
静态编译
编译器在编译可执⾏⽂件时,把需要⽤到的对应动态链接库中的部分提取出来,连接到可执⾏⽂件中去,使可执⾏⽂件在运⾏时不需要依赖于动态链接库;
动态编译
可执⾏⽂件需要附带⼀个动态链接库,在执⾏时,需要调⽤其对应动态链接库的命令。所以其优点⼀⽅⾯是缩⼩了执⾏⽂件本身的体积,另⼀⽅⾯是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只⽤到了链接库的⼀两条命令,也需要附带⼀个相对庞⼤的链接库;⼆是如果其他计算机上没有安装对应的运⾏库,则⽤动态编译的可执⾏⽂件就不能运⾏。
汇编阶段
汇编器将 hello.s
翻译成机器语⾔指令。把这些指令打包成可定位⽬标程序,即 .o
⽂件。hello.o
是⼀个⼆进制⽂件,它的字节码是机器语⾔指令,不再是字符。前⾯两个阶段都还有字符。
链接阶段
⽐如 hello
程序调⽤ printf
程序,它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于⼀个名叫 printf.o
的单独编译好的⽬标⽂件中,这个⽂件将以某种⽅式合并到 hello.o
中。链接器就负责这种合并。得 到的是可执⾏⽬标⽂件。链接也可以分为动态链接和静态链接。下面简单介绍,更多介绍可参考《操作系统-精髓于设计原理》读书笔记中的附录 7A 加载和链接。
静态链接
静态连接库就是把 (lib) ⽂件中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需要其它的库⽂件;
动态链接
动态链接就是把调⽤的函数所在⽂件模块(DLL)和调⽤函数在⽂件中的位置等信息链接进⽬标程序,程序运⾏的时候 再从 DLL 中寻找相应函数代码,因此需要相应 DLL ⽂件的⽀持。