一词三问
linux进程地址空间布局
“分段”:把程序中的各种数据,分不同的区域(“段”)来存储。
linux对进程的数据进行分段管理,不同属性的数据,存储在不同的“内存段”
不同的“内存段(内存区域)”的属性及管理方法不一样。
- .text 段
主要存放 代码(指令)
只读并且共享,这段内存在程序运行期间(进程存活时间),不会释放。
"代码段"的生存期: 随程序持续性(随进程持续性)
- .data 段
数据段。
主要存放程序中的 已经初始化的全局变量 和 已经初始化的static 变量。
可读可写,这段内存在进程运行期间,一直存在。
.data 段的生存期: 随进程持续性。
- .bss段
数据段。
主要存放程序中的 未初始化的全局变量 和 未初始化的static变量。
.bss段在进行运行前,系统会初始化 0。
可读可写,这段内存在进程运行期间,一直存在。
.bss 段的生存期: 随进程持续性。
int a = 5;
int c;
void func(void)
{
static int b = 6;
}
int main()
{
static int d;
}
上面这个例子, 全局变量 a 和 static变量b 保存在 .data段中
全局变量 c 和 static变量d 保存在 .bss段中。
- .rodata 段
read only data 只读数据段
主要是存放程序中的只读数据(常量),如: 字符串常量 “abcde”
只读。这段内存在进程运行期间,一直存在。
.rodata的生存期:随进程持续性
- 栈 空间(stack)
主要存放 局部变量(非static的局部变量)
可读可写。 栈空间,会自动释放(代码块执行完了,代码块中局部 变量的空间变会自动释放)
栈空间的变量的生存期: 随代码块持续性。
// int m = 250;
// int sb[m];
void func()
{
int a = 5;
a++;
printf("a = %d\n", a);
}
int* get_addr()
{
int x = 100;
//....
return &x;
}
//NOTE:
// "返回一个局部变量的地址 是有问题"
int main()
{
int n;
scanf("%d");
int sb[n]; //可以编译过去,但是:不建议这样搞,为什么?
// 优秀的程序员,写不出这么垃圾的代码.
func();
func();
}
- 堆空间 (heap)
动态内存空间。主要是由 malloc/realloc/calloc动态分配的空间。
可读可写。这段内存 在运行期间,一旦分配,就会一直存在,直到你手动free或进程结束。
这段空间,我们称之为 堆空间heap
堆空间只能通过 地址去访问它。防止"内存泄漏/垃圾内存"
int main()
{
int *p = malloc(1024);
//....
p = &a;
}
对象
- 在面向对象程序设计中,所有东西都可以是对象。每个对象都有自己的属性和行为。
类
-
类是同类型对象的抽象,包含该类型所有对象都具有的属性和行为。
-
在设计和编码阶段,先定义好类型,然后在类型的基础上创建对象
构造函数
-
一个类的特殊的成员函数,在任何时候创建一个对象时被自动地调用
-
初始化对象
-
构造函数的函数名必须和类名一样
构造函数没有返回类型
构造函数在创建一个类的对象时,系统自动调用它
构造函数可以带参数,也可以不带参数
一个类可以有多个构造函数,只要它们的参数互不相同
一个类必须要有构造函数
执行构造函数的第一条语句前,先执行“初始化列表”
析构函数
-
一个类的特殊成员函数,会在对象销毁时,自动调用它
-
为了在销毁对象时,自动回收一个对象的资源
在对象销毁(对象的生命周期结束)时,系统自动调用这个析构函数
-
析构函数的函数名为:~类名()
-
析构函数不带参数,也没有返回值(void也不需要)
-
一个类只能有一个析构函数
new/delete和malloc/free的区别
- new和malloc都是在堆中分配空间
- new是堆中创建一个对象,并返回其指针
- malloc仅在堆中分配一块空间,并返回其地址
- new的时候,会调用对象的构造函数;malloc你不会
- new分配的对象,销毁时必须要用delete
- malloc分配的空间,释放时必须要用free
- new/delete是运算符,malloc/free是一个库函数
封装Encapsulation
- 类把对象共有的属性和行为组织在一个类里面,并且规定不同的属性和行为的访问权限(private/protected/public),这就是封装
- 把操作方法(成员函数)和数据(属性)封装在一个对象(类)里面
类的静态成员
- 一个类的所有对象之间共享数据,而不是所有对象共享数据(其他类的对象不能共享);保证数据的安全。
- 为了实现类的对象之间数据共享,同时又能保证数据的安全,提出了这个概念
- 用static修饰的类的成员,称为类的静态成员
- 静态成员不属于某个对象,而是属于这个类的所有对象,“属于类”
- 静态数据成员的初始化,必须放在类外:
类型 类名::静态数据成员名 = 初始值;
静态成员函数
-
加了static修饰的成员函数,称之为类的静态成员函数
-
static和非static成员函数的区别
含义上的区别:
- 加了static表示这个成员函数属于类
- 不加static表示这个成员函数属于对象
static成员函数没有this指针
- static成员函数中,不能访问非static成员,非static成员属于对象
访问方式的区别:
- 类的非static成员函数,只能通过对象名或对象指针去访问
- 类的static成员函数,可以用对象名或对象指针去访问,还可以用“类名::函数名”访问
类的非static成员函数,第一个参数其实是隐含的“this指针”
类的非static成员函数,后面可以用const修饰,类的static函数不能用const修饰(const其实修饰的是this)
静态成员的访问方法
- 通过静态成员函数去访问静态成员变量
- 可以通过非static成员函数去访问静态成员
- 不能通过static成员函数去访问非static成员变量
引用
- 引用即别名,是一个已经存在的数据对象的另外一个名字
- 语法:
类型 &别名 = 已经存在的数据对象(变量/对象);
- “类型”:引用对象的类型,已经存在的数据对象的类型
- 引用在声明时,必须同时初始化
- 引用一旦声明,就不能更改
- 引用不对应“独立的内存空间”,是一个已经命名的空间的另外一个名字而已
- 引用的好处:
- 更安全
- 更容易
- 可以避免“内存拷贝”
- 主要用于:
- 函数形参
- 函数返回值
引用和指针
- 引用是别名,指针保存的是指向对象的地址
- 引用本身是没有地址的,指针变量本身是有地址的
- 指针是可以载赋值的,引用则不行
- 指针可以被赋值为nullptr(NULL),但是引用不可以被赋值为空
- 指针可以有多级指针,引用则只有一层引用
拷贝构造函数
-
也是类的构造函数,作用是:初始化对象
-
拷贝构造函数初始化对象的方式,是通过“一个已经存在的对象去初始化新对象”
-
函数原型:
T(const T& a){}
,参数为带一个本类对象的常引用 -
调用拷贝构造函数的情况
-
声明一个新对象时,()/{}内是另外一个已经有的对象
A t2(t1);
-
声明一个新对象时,用一个已经存在的对象来赋值
A t2 = t1;
-
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)
- 深拷贝不仅拷贝当下的对象内存,对象中指向的空间或资源(以及指向的空间的n次方)都会重新分配并拷贝
- 浅拷贝之拷贝当前的对象内存,对象中指向的空间没有重新分配及拷贝
右值引用
- 左值引用:引用到一个具体的“可寻址的内存”上的引用,左值引用
- 右值引用
- 引用到一个右值(“只能读”)的引用,称为右值引用
- 右值引用,就是必须绑定右值的引用,引用一个右值
- 右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
- 右值引用也是某个对象的另外一个名字
- 右值引用会为字面常量值,创建一个临时空间
- 右值引用把你的临时对象的作用域或生存期扩大或延长
- 右值引用主要是用来绑定一个将要销毁的对象上,使用右值引用的代码可以自由接管所引用的对象的资源
- 语法:
(引用对象的)类型 && 引用名 = 所引用的字面常量或临时对象;
- const引用
- 常引用:引用到一个常量值,表示我只是“读你的值”
- 把一个左值引用 引用到 常引用 是可以的
- 把一个右值引用 引用到 常引用 也是可以的
移动构造函数
- 移动语义:当用一个“临时对象(做完马上被销毁)“去构造一个新对象时,当新对象构造完成时,”临时对象“马上被销毁。
- 移动:把临时对象的资源直接移交给新对象
- 如果编译器检测到构造新对象时,传进来的那个对象是一个“马上要被销毁的对象”->“临时对象“,构造函数需要接收一个”临时对象的引用“->右值引用
- 原型:
T(T &&) noexcept{}
- 调用时机:当用一个“即将被销毁的对象”,去构造一个新对象时,编译器自动选择调用你的移动构造函数
- 实现要求:让新对象去接管临时对象的资源
友元
- friend”朋友“,友元机制是对类的封装机制的一种补充。
- 友元机制使得另一个类或函数,可以访问一个其他类对象的private/protected成员
- 友元函数/友元类
友元函数
- 如果一个函数func是另外一个类A的友元(朋友)
- 那么这个函数func就可以访问类A对象的private/protected成员
- 此时,把函数func称为类A的友元函数
- 声明:
friend 函数声明;
友元类
- 如果类A是类B的朋友,那么类A的所有成员函数(“类A的作用域中”),都可以访问类B的对象的private/protected成员
- 声明:
friend class 类名;
- 把上述声明放在指定的类B中,那么这个类就是类B的友元
友元传递
-
友元关系的单向的
类A是类B的友元,无法退出:类B是类A的友元
-
友元关系是不可传递的
类A是类B的友元,类B是类C的友元,不能推出:类A是类C的友元
命名空间
- 命名空间的本质是一块内存(”作用域“),用来存放一些常量、变量、函数、类型等
- 目的:避免“名字冲突”
- 语法:
namespaece 命名空间的名称{/*常量、变量、函数等定义或声明*/};
- 如果定义的namespace需要被多个文件包含,则需要把声明和定义分开。如果不分开,则会报“xxx被多次定义 multiple definition”。声明放在头文件中,定义或实现放在相应的cpp中,以防止重复定义
命名空间的使用方法
-
使用“作用域限定符::”来访问命名空间中的成员
-
using声明
-
无名字的命名空间
定义一个命名空间时,不指定名字
特点:在“无名命名空间”中定义的常量、变量、函数等对象被设置为全局变量,违背了命名空间的设定原则,所以一般不使用无名命名空间。程序中定义的全局变量,会自动加入到无名命名空间中去,要访问“无名命名空间中的成员”,要用
::成员名
继承Inheritance
- 一个类自动得到其父类(基类,另外一个类)的所有属性和行为
- 目的:代码复用,实现快速开发
- 语法:
class 派生类名 : 继承方式 基类名 {, 继承方式 基类名2, ...}{};
- 继承方式:决定派生类对基类继承过来的成员的访问权限,基类成员的可见性
三种继承方式的区别
Base Class成员 | public继承 | private继承 | protected继承 |
---|---|---|---|
private members | 不可见 | 不可见 | 不可见 |
protected members | protected | private | protected |
public members | public | private | protected |
- 派生类虽然继承了基类所有属性和行为,但不一定能访问它们,基类的private,派生类就不能访问
派生类不能访问的
- 基类的私有成员,派生类不能访问
- 基类的友元,派生类不能继承
- 基类的赋值运算符,派生类不能继承
- 基类的构造函数和析构函数,派生类不能继承
派生类访问基类的构造函数和析构函数
- 系统会自动调用合适的构造函数和析构函数
- 派生类的构造函数自动调用基类的默认构造函数(无参构造或等同于无参构造),编译器不知道如何调用基类的构造函数,因为要传递参数
- 显示调用基类的非默认构造函数:派生类构造函数的初始化列表中,可以指定调用基类的任意构造函数:
derived_constructor(xxx): base_constructor(yyy){}
多继承
- 一个派生类可以同时继承多个基类的属性和行为
- 语法:
class 派生类名: 继承方式 基类A, 继承方式 基类B, ...{};
派生类、基类、组合对象构造函数调用顺序
- 一个对象的内存布局(数据成员,non-static数据成员)
- 继承是is a的关系,组合对象是has a的关系
- 先布局基类的数据成员(按继承的先后顺序,依次分配空间),然后才是派生类自己的数据成员(按成员的声明顺序,依次分配空间)
- 调用顺序:按继承先后调用基类的构造函数→按成员的声明顺序,依次调用各数据成员的构造函数
- 按内存布局的先后,依次调用相应的构造函数
- 调用析构函数的顺序则刚好相反
基类与派生类赋值兼容的问题
- 派生类对象就是一个基类对象
- 程序中所有需要基类对象的地方,可以用派生类替换
-
可以用派生类对象去初始化基类对象
因为派生类对象中,包含基类的数据成员
-
可以把派生类对象赋值给基类对象
等于号右边的对象类型一定要兼容左边的那个对象
-
可以把派生类对象初始化为基类的引用
-
基类指针可以指向派生类对象
多态Polymorphism
-
不同的对象,在收到相同的信息时,产生不同的行为;指向类型不同的对象,在收到相同的消息,实现不同的算法功能。
-
具体表现:
编译时刻呈现的多态
- 函数重载
- 运算符重载
运行时刻的多态
- 虚函数
重载Overloading
- 在C++中,如果我们创建两个或以上的成员,它们有相同的名字,但是参数个数或类型不一样,这就是“重载”,“名字相同,参数不同”
函数重载
-
在同一个作用域上,多个函数同名,但参数个数或类型不一样
C++中,const int 和 int 是不同的类型
-
目的:提高代码的可读性和可维护性
通过函数重载,程序员可以使用相同的名称来表示不同的操作,而不必记住多个不同的名称
-
为了支持函数重载(多个同名函数,但参数不同),采用了一种叫“换名机制”的东西。重载函数在经过编译器编译后,编译器会结合参数的个数及类型,形成一个新的函数名,这种机制称为C++编译器的“换名机制”
-
引起函数重载歧义的原因
- 类型转换引起的歧义
- 带默认参数的函数重载,引起的歧义
- 带“引用参数”的函数,引起的歧义
extern “C”
- 混合编程时,C++采用了换名机制,C语言没有换名机制
- 目的:为了支持C++与C混合编程
- 加上extern "C"后,会指示编译器这部分的代码按C语言,而不是C++的方式进行编译
运算符重载 Operator Overloading
-
赋予运算符接不同类型操作数的功能
-
目的:方便程序员使用自定义类型,合适的操作符重载可以使自定义类型的操作像内置类型一样自然
-
所有的运算符都是通过函数来实现的,运算符重载其实就是函数重载
-
运算符函数的函数名为:operator运算符
-
运算符函数的参数为:运算符的操作数
-
运算符函数的返回值:根据运算符表达式的值的类型来定
-
分为两类:
-
全局函数
return_type operator运算符(arguments){ }
-
类的成员函数
class T{ return_type operator运算符(arguments){ } };
-
-
-
只能实现为类的成员函数的:
- 赋值运算符 =
- 下标运算符 []
- 函数调用运算符 ()
- 指针运算符 ->
-
不能重载的运算符:
- 条件运算符 ?:
- 成员访问运算符 .
- 域运算符 ::
- 长度运算符 sizeof
- 成员指针访问运算符 -> 和 .
-
特殊的运算符重载
-
自增/自减运算符重载
class T{ //前置++ T& operator++(){ //先完成“自增” return *this; } //后置++ T operator++(int){ T tmp(*this); //完成“自增” return tmp; } //前置-- T& operator--(){ //先完成“自减” return *this; } //后置-- T operator--(int){ T tmp(*this); //完成“自减” return tmp; } };
-
输入输出运算符重载
输入输出运算符只能重载为 全局函数
输出运算符的第一操作数是类 ostream 对象
输入运算符的第一操作数是类 istream 对象
class Complex{ friend ostream& operator<<(ostream&, const Complex&); friend istream& operator>>(istream&, Complex&); };
-
赋值运算符(=)重载
赋值运算符函数只能重载为 类的成员函数
- 防止“自赋值”,如:c1 = c1
- 当时一个Deep copy时,重新分配资源,实现“Deep Copy”
- 返回自身对象的应用。return *this
class T{ T& operator=(const T& other){ //1.防止“自赋值” if(this == &other){ return *this;; } //2.根据具体的情况,实现“内存拷贝” //3.返回自身对象的引用 return *this; } };
-
函数调用运算符()重载
把一个对象当做是一个“函数调用的对象”
函数调用运算符只能重载为 类的成员函数
class T{ return_type operator()(形参列表){ } bool operator()(const double& x1, const double& y1) const{ return x1 * k + b == y1; } };
- 函数对象Funcator “仿函数”
- 在C++中,所有实现了“函数调用运算符()”的类的对象,我们称之为“函数对象”。这个对象,可以当函数使用
- 函数对象的优势在于:
- 函数对象本身可以保持一些状态或属性信息
- 普通函数或函数指针,把操作数据的指针和数据是分开的,函数对象用法比较方便
-
函数隐藏Function Hiding
-
函数隐藏发生在派生类和基类当中,假如派生类中定义了一个函数与基类的函数同名(参考可以不同),基类相应的函数就会被隐藏
通过派生类的实例,就不能调用到基类的相应函数
-
目的:派生类刻意为之。派生类可能对同一功能,有更好的实现或优化,希望派生类对象,不要再去调用基类的实现
-
被隐藏后,调用基类被隐藏的函数的方法
- 派生类对象.基类名::基类的函数名
- 通过 using 声明
函数重写Function Overriding
- 派生类中定义了与基类一样的函数(函数名相同、参数也相同)
- 尽管重写了父类的函数,但是派生类中仍然可以通过“父类名::函数名”的方式,去调用父类被重写的函数
- 通过虚函数表VMT(Virtual Method Table)实现
- 含有虚函数的类的对象内存的最前面,自动添加一个指针vptr,指向这个虚函数表。
- VMT 虚函数指针的数组
- 函数指针的数组 有多少个虚函数,这个数组里面就要多少项
- 每一项保存的是虚函数的实际(重写)地址
虚函数Virtual Function
- 在基类中用关键字“virtual”修饰的成员函数,在派生类中可以重写的成员函数
- 目的:告诉编译器,对这个函数进行一个“动态绑定/延迟绑定”
虚函数的规则
-
虚函数必须是一个类的 非静态成员函数
-
虚函数的访问必须通过对象的指针
-
虚函数必须在基类,即使它在基类中不使用
-
派生类和基类对于虚函数的原型(函数名、参数)必须完全一致,否则就不是函数重写
-
构造函数不能声明为虚函数,但是析构函数可以声明为虚函数
构造函数和析构函数 所有的动态绑定都失效
构造函数和析构函数 只能进行静态绑定
纯虚函数Pure Virtual Function
-
只有声明没有实现的虚函数
-
语法
class T{ virtual 返回类型 函数名(参数列表) = 0; };
-
包含纯虚函数的类,称为抽象类
-
抽象类不能实例化对象
因为抽象类含有纯虚函数,没有实现,没有对象
-
抽象类的作用
通过公共的特征和接口,供派生类去继承并实现
模板Template
Template是C++提供的强大的功能特性
允许在设计时,不指定具体的数据类型
“通用类”、“通用函数”、“通用算法”、…、“泛型编程”
Template为“泛型编程”提供支持
-
泛型编程是一种把类型当参数,目的在于让你的算法支持不同类型的数据结构
-
Template的两种形式
- Function Template 函数模板
- Class Template 类模板
-
语法:
-
只带一个类型参数
template <typename T> template <class T> return_type func_name(parameter_list){ }
-
带多个类型参数的模板
template <class T1, class T2, ...> return_type func_name(parameter_list){ }
-
类模板Class Template
-
设计类的时候,带“泛型参数”
-
模板:
-
一个类型参数的类:模板类名<具体的类型>
template <class T> class class_name{ T data; };
-
带多个类型参数的类模板
template <typename T1, typename T2, ...> class class_name{ }; //实例化一个对象,或指定类名时 class_name<T1, T2, ...> object_name;
-
-
模板的声明和实现必须在同一个文件中
迭代器iterator
- 一个任意的对象,指向一个范围(数组,链表)内的元素
- 迭代器有遍历这个范围内每个元素的能力,因为迭代器对象实现了一些必要操作(如:++,*)
迭代器模式
- 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象内部结构的表示