数据类型
变量所占大小
X64X86char1字节1字节short2字节2字节int4字节4字节long8字节4字节long long8字节8字节float4字节4字节double8字节8字节long double 通常16字节通常8字节
char
分为三类型:
1.char:标准类型
2.unsigned char:无符号char(0~255)
3.signed char:有符号char(-128~127)
构造类型
数组
1.一维数组
**字符串/字符数组**
char arr1[] = "hello"; 字符串:结尾有'\0'终止符,arr1占6字节
char arr2[] = {'H', 'e', 'l', 'l', 'o'}; 字符数组:结尾无'\0',arr2占5字节
'\0'影响printf等输出,输出结果为'\0'之前
**字符串的申明方式**
1.静态分配,编译时在栈上分配好内存
2.字符串常量
3.动态分配内存(malloc和new)
4.自动分配,在局部函数中
**操作函数**
size_t strlen(const char *str) 返回字符串长度,不包含'\0'
char *strcpy(char *dest, char *src) 把src复制给dest
char *strncpy(char *dest, const char *src, size_t n) 把src复制给dest前n个字符
char *strcat(char *dest, const char *src) 把src添加到dest的结尾
char *strncat(char *dest, const char *src, size_t n) 把src前n个字符添加到dest结尾
int strcmp(const char *str1, const char *str2) 比较字符串是否相同,成功返回0,失败返回不同个数
int strncmp(const char *str1, const char *str2, size_t n) 比较前n个字符串,返回与strcmp相同
char *strchr(const char *str, int c) 返回c第一次在str出现的地址
char *strrchr(const char *str, int c) 返回c最后一次在str出现的地址
char *strstr(const char *haystack, const char *needle) 返回needle在haystack第一次出现的地址
**地址**
char arr[10] = "Hello";
arr与&arr[0]所表示是一样的
&arr是整个数组的地址,类型是char (*)[10],表示10个指向char元素的指针
**指针数组与数组指针**
指针数组,包含3个char的指针
char *ptr_arry[3]
数组指针,指向一个包含3个char的数组
char (*arry_ptr)[3]
2.二维数组
**初始化**
1.没什么好说的
int array[3][4];
2.编译器自动分配
int array[][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
3.分配动态内存
int rows = 3;
int cols = 4;
int **array = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
array[i] = (int *)malloc(cols * sizeof(int));
}
4.使用stl容器
int rows = 3;
int cols = 4;
std::vector<std::vector<int>> array(rows, std::vector<int>(cols));
**地址 **
** **&a[1],&a[0]==a;一维数组地址
&a[0][1],&a[0][0] == a[0];元素地址
&a;二维数组地址
结构体
**声明**
1.默认声明
struct Point {
int x;
int y;
};
C++的结构体class,为兼容c,保留了struct
2.定义并声明实例
struct Point {
int x;
int y;
} p1, p2;
3.结构体别名
typedef struct {
int x;
int y;
} Point;
Point p1;
C++默认声明,创建实例时不需要声明结构体struct或class
**结构体大小**
struct Example {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
以上面为例,变量存储地址为自身所占字节大小的倍数,设从0x00开始,char占1个字节,地址在0x00,b占4字节,因为地址要是自身字节大小的整数倍,所以地址在0x04;c占1个字节,地址在0x08,所以Example结构体的大小为9字节。
其中,c特有的位域,变量+:+位数,用于修改变量大小,节省内存空间。计算方式为同类型变量合并,不足该变量字节大小的部分自动填充,不同类型的变量按结构体的计算方式存入地址。
**链表**
**单链表**:简单,节点只包含指向下一个节点的指针。
** 双向链表**:支持双向遍历,节点包含指向前一个和下一个节点的指针。
** 循环链表**:尾节点指向头节点,形成一个环,适合需要循环访问的情况。
**C++中class与struct区别**
(1)相同点
都能拥有成员函数,公有和私有部分;class可以实现的struct也可以实现。
(2)不同点
struct默认公有,class默认私有;struct默认公有继承,class默认私有继承。
(3)C++的struct与C的区别
C语言中struct是自定义数据类型;C++中是抽象数据类型,支持成员函数的定义(C++中可以继承和实现多态)
C语言中struct没有访问权限设置,成员只能是变量,但可以存入函数地址,数据不能被隐藏。
C++设置了访问权限,功能与class一样,默认是public访问。
C语言声明实例时必须在前面加struct,除非定义结构体时使用typedef。C++不需要,结构体struct在C++中被当作特例。
联合体
**联合体声明**
union Data {
int i;
float f;
char str[20];
};
**联合体大小计算**
联合体的大小是其最大的数据成员大小的整数倍再满足编译器的内存对齐要求的最小倍数。以上面为例,联合体最大成员时char str[20],则联合体大小为20字节,若内存对齐要求为4字节,则不变;若为8字节,则填充4字节为24字节。
** 判断大小端**
#include <iostream>
union data{
int a;
char b;
}
void main()
{
data udata;
udata.a = 0x12345678;
if(udata.b == 0x78)
printf("Little-Endian\n");
else if(udata.b == 0x12)
printf("Big-Endian\n");
else
printf("Unkonwn Endian\n");
}
指针
内存的申请与释放
C++中的new
分配内存,构造一个整数
int* ptr = new int;
分配内存,构造函数参数进行构造
MyClass* ptr = new MyClass(arg1, arg2);
分配数组内存
MyClass* array = new MyClass[5];
或者memset设置内存初始值
使用std::nothrow,new失败时返回null
malloc
type p = (type)malloc(分配大小)
分配失败时返回null
malloc与new的异同
** 相同点**
都可以动态申请内存
**不同点**
new是C++操作符,支持重载,还会调用构造函数;malloc是C/C++的标准函数。
new是类型安全的,malloc不安全。
new返回具体指针,malloc返回void型指针,需要类型转换。
new自动计算分配内存大小,malloc需要手动计算。
new是封装了malloc。
C++中的delete
**释放内存 **
释放单个对象的内存
MyClass* obj = new MyClass(); // 用 new 分配对象
delete obj; // 释放分配的对象内存
释放数组的内存
MyClass* array = new MyClass[10]; // 用 new[] 分配对象数组
delete[] array; // 释放分配的数组内存
一个new对应一个delete
释放内存后需要将指针指向空
delete null是安全的
free
**释放内存**
int *array = (int *)malloc(10 * sizeof(int));
free(array);
一个malloc对应一个free
内存释放后将指针指向null,避免产生野指针
free(NULL)会崩溃
new和delete是如何实现的
**new的实现过程**:对简单的函数直接使用operator new函数;对复杂的数据结构调用operator new函数,分配足够大的原始为类型化的内存,运行该类型的构造函数并传入初始值,最后返回该对象的指针。
**delete的实现过程**:简单数据类型直接调用free;对复杂的数据结构对指针指向的对象运行析构函数,再用operator delete函数释放对象所使用内存。
new[]一个数组对象,需要知道数组的长度,会多分配4个字节,实际的数组所占内存为p-4;delete[]操作会取出这个数,知道要调用多少次析构函数。
malloc和free的实现
这两个函数是由brk、mmp和munmap这些系统调用实现的。
**brk**是将堆顶的指针向高位移动,获得新的内存空间。**mmap**是在进程的**虚拟地址空间(堆和栈中间,称为文件映射区)**中找到一块空闲的内存块。这两种都是分配虚拟内存,没有分配实际的物理内存。在第一次访问已分配的虚拟地址空间,发生**缺页中断(当程序访问的虚拟内存页面不存在物理内存时,会触发缺页中断)**,操作系统分配物理内存,建立虚拟内存与物理内存的映射关系。
malloc分配内存时,当分配内存小于128k,则使用brk在堆顶向高地址移动指针;当分配的内存大于128k时,使用mmap在虚拟地址空间寻找一块空闲内存。brk分配内存需要等高地址的内存释放后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K,则会执行内存紧缩。
被free回收的内存是立即还给操作系统了吗
不是的,被free回收的内存会用双链表ptmalloc保存,当下次申请内存的时候就尝试再内存中寻找合适的返回,避免反复的系统调用,同时tpmalloc也会尝试合并小块内存,防止产生过多内存碎片。
calloc与realloc
calloc省去了人为空间计算,calloc申请的空间的初始值是0;realloc给动态分配的空间分配额外的空间。
** 深拷贝和浅拷贝**
** 浅拷贝**
class a{
public:
char *data;
a(const char* str){
data = new char[strlen(str)+1];
strcpy(data, str);
}
a(const a &other) : data(other.data) {} //浅拷贝构造函数
a();
~a();
}
int main()
{
a a1("hello world!");
a a2 = a1;
return 0;
}
** **a1直接赋值给a2,实际a2使用的内存与a1使用的是同一块,a1内存被回收后,a2会访问无效内存,发生未定义行为。
** 深拷贝**
class a{
public:
char *data;
a(const char* str){
data = new char[strlen(str)+1];
strcpy(data, str);
}
a(const a &other){
data = new char[strlen(other.data)+1];
strcpy(data, other.data);
}//深拷贝构造函数
a();
~a();
}
int main()
{
a a1("hello world!");
a a2 = a1;
return 0;
}
a2不仅复制了a1的值,还分配了独立内存 。
指针类型
数组指针与指针数组
数组指针:数组是指针;指针数组:成员是指针。
二级指针
一级指针指向某数据类型的内存地址,二级指针指向一级指针的内存地址。
函数指针
int (*funcPtr)(int, int);
它用于指向函数的内存地址。
指针大小
32位一般是4字节,64位一般是8字节。地址+1是加了一个类型的大小。
*的三种作用
**(1)解引用运算符:***指针变量,表示地址操作符,取内容
**(2)指针申明:**表示指针变量
** (3)运算:**表示乘
C++引用变量
声明
int b = 4;
int &a = b;
是对已存在变量的别名。
常量引用
const int &value
值不允许修改。
数组引用与指针一样。
指针的引用
int *a = new int;
int *(&b) = a;
引用与函数
不要引用局部变量。
引用与指针的区别
(1)引用声明的初始化,指针不用马上初始化
(2)引用不能指向空,指针可以
(3)引用初始化后不能指向其他变量,指针可以
(4)引用效率高
(5)引用更安全,指针可以偏移
(6)指针更灵活,直接操作地址;指针更通用,C/C++都可以。
修饰符
static的用法和作用
1.隐藏
2.保持变量内容的持久,即改变变量的生命周期。将static修饰的变量存入静态数据区(全局区)。
3.默认初始化为0。
4.在C++中类成员声明static:
(1)函数体内static变量的作用范围为该函数体,内存只分配一次。
(2)在模块内的static全局变量只能被该模块的函数访问。
(3)模块内的static函数只能被该模块的函数调用。
(4)类中的static成员变量属于类所有,对类的所有对象只有一份拷贝,该变量的初始化要在类外。
(5)类中的static成员函数属于类所有,这个函数没有this指针,只能访问类中的静态成员变量。
类内:
(6)static类对象要在类外初始化,因为static修饰的变量先类存在。
(7)static修饰的类成员属于类不属于对象,所以没有this指针,this指针是指向本成员的指针,所以无法访问非static的类成员。
(8)static成员函数不能被virtual修饰,static成员不属于任何对象和实例,virtual加上没有任何意义;静态成员没有this指针,虚函数的实现是对每个对象分配vptr指针,而vptr指针由this指针调用,所以不能为virtual。虚函数的调用关系:this->vptr->ctable->virtual function。
静态变量什么时候初始化
1.初始化只有一次,在主程序前编译器已经分配好内存。
2.静态局部变量与全局变量一样存在全局区。在C中,初始化发生在执行代码前,编译阶段分配内存后,就会初始化,所以C语言中无法用变量初始化静态局部变量;程序结束,变量所处的全局内存会被回收。
3.在C++中,初始化在执行相关代码时,主要时C++引入对象后,要进行初始化必须要用构造函数或析构函数,构造和析构函数一般需要执行相关的程序,而非简单分配内存。所以C++规定在首次使用到全局或静态变量时进行构造,并通过atexit()管理。在程序结束时,根据构造的顺序反方向析构,所以C++中的静态局部变量可以用变量初始化。
指针与const的用法
int val = 10;
指针常量
int* const a = &val;
常量指针
const int *b = &val;
指针常量是指指针是常量,即指针指向的地址不可变,但指向地址的内容可变。
常量指针是指指针指向的内容不可变,地址可变。
volatile、mutable和explicit关键字用法
volatile
定义变量的值是易变的,防止被编译器优化,声明后系统总是重新从内存中读取数据。
多线程下作用是防止优化编译器把内存装入CPU寄存器。
注意:可以把非volatile int赋给volatile int,但不能非volatile对象赋给volatile对象;用户定义的类型也可以用volatile修饰;volatile的类只能访问它的接口子集,一个由类控制者的子集,用户只能通过const_cast来获得对类型接口的完全访问。
**volatile指针**
volatile指针
volatile char *p;
指针volatile
char* volatile p;
与常量指针和指针常量类似
**volatile指针**:指针指向的内容是易变的。
** 指针volatile**:指针指向的地址是易变的。
mutable
修饰对象将永远可以修改。在const函数里修改与类状态无关的数据成员。
class person{
mutable int a;
public:
void add() const
{
a = 10;
}
};
int main()
{
const person p = person();
p.add();
p.a = 40;
return 0;
}
explicit
explicit修饰构造函数,那么构造函数不能用隐式。explicit修饰转换操作符,必须显示转换。
#include <iostream>
class MyClass {
public:
explicit MyClass(int x) : value(x) {} // `explicit` 构造函数
explicit operator int() const { return value; }
int getValue() const { return value; }
private:
int value;
};
void printValue(const MyClass& obj) {
std::cout << "Value: " << obj.getValue() << std::endl;
}
int main() {
MyClass obj1(10); // 合法:显式调用构造函数
printValue(obj1); // 合法:传递 `MyClass` 对象
int x = static_cats<int>(obj) //合法: 显示数据类型转换
// MyClass obj2 = 20; // 错误:隐式转换被禁用
// printValue(30); // 错误:无法将 `int` 隐式转换为 `MyClass`
return 0;
}
final与override
override
指定该函数为父类的虚函数重写
class A{
virtual void foo();
}
class B: public A{
virtual void fo0() override;//错误,函数一定是继承A的,找不到就报错
virtual void f00(); //正确
void foo(); //正确
virtual void foo(); //正确
void foo() override(); //正确
}
final
不希望某个虚函数被继承或重写
class Base
{
virtual void foo();
}
class A: public Base
{
void foo() final;
}
class B final : A
{
void foo() override; //错位,foo不能被重写
}
class C: B //错误,B不能被继承
{
}
define与const的区别
编译阶段
define在编译的预处理阶段起作用,const在编译、运行的时候起作用。
安全性
define只作替换,不做类型检查和计算,易产生错误;const常量有数据类型,编译器可以对其进行类型安全检查
内存占用
define只是将宏名称进行替换,在内存中会产生多份相同的备份;const只有一份备份,可以执行常量折叠,将复杂的表达式计算出结果放入常量表。
总结
宏替换发生在编译阶段之前,属于文本插入;const作用发生于编译过程;
宏不检查类型,const会检查数据类型;
宏定义的数据没有分配内存空间,只是插入替换;const定义的变量只是值不能改变,但要分配内存空间;
顶层const与底层const
**顶层const**:指const修饰的本身是一个常量,无法修改,在*号右边。
**底层const**:指const修饰的变量所指向的对象是一个常量,在*号左边。
C++类
类和对象
类
定义:具有相同属性和行为的对象的集合。
类的实例
类声明
class A
{
public:
void show()
{
cout<<a<<endl;
}
private:
int a;
protected:
};
struct B
{
public:
private:
protected:
}
声明对象
person p;
person *p = new person;
访问修饰符
关键字
private:类内可见,class内不写访问修饰符,默认private;
public:类外可见,struct默认public;
protected:类内及子类可见;
友元
友元函数
class A
{
private:
int b;
public:
A() : b(5) {}
friend void show(A &a);
}
void show(A &a)
{
cout<<a.b<<endl;
}
int main()
{
A a;
show(a);
return 0;
}
友元类
class A
{
private:
int val;
public:
A() : val(20) {}
friend class FriendA;
}
class FriendA
{
public:
void show(A &a) const
{
cout<<a.val<<endl;
}
}
int main()
{
A a;
FriendA fa;
fa.shwo(a);
return 0;
}
使用友元可以访问类的public,private和protected,继承可以访问public和protected。
** 特点**
不受访问修饰符的影响;可以有多个友元;破坏了类的封装性,不是迫不得已,不要使用。
函数成员
构造函数
产生
普通成员不能在类内直接赋值,因为只有对象的创建的时候才会分配空间,构造函数就是对数据成员赋值。
形式
类名(参数列表){}
class A
{
public:
A() {}
A(int b) {val = b;}
A(int b, string str) {s = str;val = b;}
private:
string s;
int val;
}
调用
声明栈区对象时若没有特指构造函数则会调用默认构造函数;声明堆区对象不会调用构造函数,是在new空间的时候调用。
类型
** (1)默认构造函数**
什么都不做,即为空的。只要宏观声明的构造函数,默认的就没有了。
** (2)无参数**
** (3)有参数构造函数**
通过对象传递赋值;可以指定默认值;
** (4)多个构造函数构成重载**
class A
{
public:
A() : val(10) {}
A(int b) : val(b) {}
private:
int val;
};
初始化列表
**形式**
构造函数后加冒号。
class A
{
public:
A() : val(10) {}
private:
int val;
};
**作用**
和构造函数的区别:基本数据类型都行,引用与const必须用初始化列表。
**执行顺序**
在构造函数之前。
** 数组和结构体初始化**
数组通常设置全元素为0的方法,如memset或for循环;结构体直接赋值。
析构函数
作用
清理工作:new一个对象,析构函数可以释放掉。
形式
~类名() 只有一个,没有参数,没有重载。
调用
对象生命周期结束时自动调用:局部变量在函数结束时结束,临时对象在作用域内,指针对象在delete时结束。
new、malloc、delete、free区别
new自动调用构造函数,malloc不行;delete自动调用析构函数,free不行。
常函数
形式
析构与构造函数不能是常函数。
void fun() const {......}
特点
可以使用数据成员,但不能修改;常对象只能调用常函数,不能调用普通函数;常函数的this指针类型是const 类名*;
static
使用方式
对象调用;类名作用域。
** 静态成员**
类外初始化,初始化时不加static。
静态常量数据成员可以直接在类内初始化。
静态成员函数
没有this指针,不能被virtual修饰;不能调用普通成员,可以调用静态成员;是类的属性,不是对象,所有对象共享一个;
拷贝构造/赋值构造
形式
class A
{
public:
A(const A &a);
}
何时调用
** (1)新建一个对象**
MyClass mc;
MyClass mc1(mc);
MyClass mc2 = mc;
MyClass mc3 = MyClass(mc);
MyClass *mc4 = new MyClass(mc);
** (2)当程序生成对象副本**
函数参数传递对象的值;函数返回对象。
功能
默认的复制构造函数,逐个复制非静态成员的值,复制的是成员的值,又叫浅拷贝。
深拷贝
指针成员不能直接赋值,要用内存拷贝memcpy,strcpyd等。
解决拷贝构造引发的指针成员二次释放崩溃问题
深拷贝、传地址、传引用。
为什么拷贝构造函数必须传引用而不能传值
1.构造拷贝函数的作用是复制对象,在使用这个对象的实例来初始化这个对象的一个新的实例
2.参数传递的过程中,将地址传递和值传递统一起来;值传递:对于内置数据类型的传递时,直接赋值拷贝给形参,对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);引用传递:无论对内置类型还是类类型,传递引用或指针最终都是传递的地址,因此不会像类类型值传递需要调用构造函数。
值传递会先对局部变量类的实例调用拷贝构造函数来初始化实例,会产生无限递归,栈溢出。
内联函数
常规函数调用过程
调用时,根据函数地址跳到代码空间执行代码,执行完再跳转到调用位置;综合看:来回跳跃+记录跳跃位置,需要消耗一定的系统开销。
内联函数
inline:函数声明要加inline,函数定义要加inline,只写声明位置没用。
作用:用相应的代码代替调用,比常函数稍快,代价是占用更多内存。
常规函数的内联函数如何选择
特点:时间和空间看性价比和实际需要。
实际:函数代码少,流程直接,调用频繁的选择内联函数。
编译器智能:程序员请求把函数作为内联函数,编译器不一定答应;如函数体过大,递归不能是内联函数,效果就是写了等于没写。
内联函数比宏功能更强
类型安全:内联函数有类型检查。
调试友好:内联函数在调试时能有更多信息。
类与内联函数
类内定义的函数都是内联函数,不写的就是隐式inline,写就是显示inline,写不写都行。
定义在外的有inline是内联,没有inline不是内联。
内联函数与多文件
内联函数可以有多个定义,多个定义必须完全一致,所以通常在内联函数写在头文件。
数据成员
相对特殊
引用、const成员,初始化列表初始化;静态成员,类外初始化,无this指针;静态常量成员,类内和类外初始化;指针成员,注意拷贝构造产生的问题。
this
定义:this是一个指针,指向当前调用成员函数的对象。
作用:this指针使得成员函数能够访问和操作对象的成员数据和其他成员函数。对象创建时才有的。
类型:对应对象的类的指针类型。
this指针不是成员,this的作用域在类内,系统默认传递给函数的隐含参数。
继承
继承实例
继承的作用
代码复用,避免写重复的代码。
继承的格式
people为基类或者称父类,xiaoming为子类或称派生类。
class xiaoming : public people
{
......
}
继承对象的声明以及成员调用
普通对象:调用构造函数或者缺省构造,调用成员形式:类名.成员;
指针对象:new分配内存,调用形式:类名->成员;
基类也可以自己创建对象;
继承的限定词
private:父类中的public和protected在子类为private,降低访问权限;
protected:父类的public成员在子类为protected,降低权限;
public:父类的权限是什么样的在子类就是什么样的;
访问publicprotectedprivate同一个类yesyesyes(public继承)派生类yesyesno外部类yesnono
成员的继承
函数成员
** 构造函数**
父类的构造函数可以通过子类构造函数的初始化列表调用。子类和父类的构造函数执行顺序是先父类再子类;
class base
{
public:
base(int x): val(x) {}
base(double): val(static_cast<int>x) {}
private:
int val;
}
class Derived : public base
{
using base::base;
Derived() : base(0), str('c') {}
}
int main()
{
Derived d(1);
Derived d(3.14);
Derived d();
return 0;
}
** 析构函数**
按顺序调用,辈分由小到大调用,与构造的顺序相反。
** 覆盖**
父类和子类出现同名成员时,会覆盖;类内:子类覆盖父类,可以通过作用域区分;类外:类名作用域区分;父类子类函数相同名没有重载关系;
** 友元**
友元函数不能被继承。
虚继承
用于解决多重继承的菱形问题,虚基类只会保留一个实例,解决多继承中访问不明确的问题,结构复杂,内存开销比较大;
#include <iostream>
class A {
public:
A() { std::cout << "A constructor" << std::endl; }
virtual ~A() { std::cout << "A destructor" << std::endl; }
int value;
};
class B : virtual public A {
public:
B() { std::cout << "B constructor" << std::endl; }
virtual ~B() { std::cout << "B destructor" << std::endl; }
};
class C : virtual public A {
public:
C() { std::cout << "C constructor" << std::endl; }
virtual ~C() { std::cout << "C destructor" << std::endl; }
};
class D : public B, public C {
public:
D() { std::cout << "D constructor" << std::endl; }
virtual ~D() { std::cout << "D destructor" << std::endl; }
};
int main() {
D d;
d.value = 42;
std::cout << "d.value = " << d.value << std::endl;
return 0;
}
菱形问题
D继承B、C,重复了基类的实例;同时D通过B、C两个路径访问A,会存在二义性;
A
/ \
B C
\ /
D
多态与虚函数
多态指对象在不同的情况下表现出不同的行为,虚函数是实现这个思想的语法基础。
多态分为两种,动态多态和静态多态;**动态多态**:主要由虚函数实现,运行时决定调用哪个函数;**静态多态**:主要由函数重载和运算符重载实现,由编译器决定调用哪个。
多态是一种泛型编程思想,使程序在不同类型的对象以一种统一的方式进行操作。
父类指向子类空间
如下代码所示,a作为基类指针指向B的空间,a->show()调用的是B的函数,是通过虚表调用的;但是,a不可以访问B的showb()函数,showB()在A这是未知的。
一个父类指针可以有多种执行状态,成为多态。
class A
{
public:
virtual void show {cout<<"A"<<endl;}
}
class B : public A
{
public:
virtual void show {cout<<"B"<<endl;}
void showB {cout<<"show B"<<endl;}
}
int main()
{
A* a = new B();
a->show();
return 0
}
特点
1.重写
子类与父类的函数名字参数相同,父类函数声明virtual,子类重写父类的函数,可以加virtual也可以不加。
返回值类型相同为重写,返回值类型不同为协变;
2.不是内敛函数
3.构造函数不能是虚函数
虚函数依赖虚函数表的初始化,在对象的构造过程中,虚函数表未完全设置。调用构造函数还不能确定对象的真实类型;构造函数的作用是初始化,在类的生命周期中只执行一次,不是对象的动态行为,没有必要成为虚函数。
在构造函数中可以调用虚函数,此时的虚表指针指向该类的虚表,调用的函数是正在构造的类中的虚函数,而不是子类的虚函数。
析构函数可以设置为虚函数:一般情况基类的析构函数要定义为虚函数,只有在基类析构函数为虚函数,基类指针调用delete操作符销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上调用虚函数),防止内存泄漏,析构函数可以是纯虚函数。
虚表
虚表的覆盖原理
根据父类指针找到父类的函数,判断父类的函数是不是虚函数,若是,则进入虚表执行重写的;详见父类指向子类空间的代码。
虚表的地址:对象空间开头的四字节内容是虚表地址。
实现多态的过程
(1)编译器发现基类中有虚函数,会自动为每个有虚函数的类生成一份虚表,表是一维数组,需表里保存了虚函数的入口地址。
(2)编译器在每个对象的前四个字节保存一个虚表指针,*vptr,指向对象所属类的虚表。在构造时,根据对象的类型取初始化虚指针vptr,让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。
(3)在派生类定义对象时,程序会自动调用构造函数,在构造函数创建虚表并对虚表初始化。在构造子类的对象时,会先调用父类的构造函数,此时编译器只看到父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表。
(4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向基类虚表;当派生类对基类的虚函数重写时,则派生类的虚表指针指向自身的虚表。
虚表函数位于只读数据段(.rodata)即常量区,虚函数位域代码段(.text)即代码区。
纯虚函数
有纯虚函数的就是抽象类,抽象类不能被实例化,只能被继承。
没有函数,继承这个基类的子类必须实现函数,才能被实例化,不然也是抽象类。
接口类:全是纯虚函数,可以有构造函数和成员。
virtual void fun() = 0;
模板
函数模板
意义:模板是泛型编程的一种重要思想。stl就是利用模板实现的一个具体实例。
template <typename T>
void fun(T t)
{
cout<<t<<endl;
}
也可以多个参数
template <typename T, typename B>
void fun1(T t, B b)
{
cout<<t<<b<<endl;
}
typename和class可以相互替换
作用域:仅对下面挨着的代码段有效。
具体化
template <typename T>
void fun(T t)
{
cout<<t<<endl;
}
template <>
void fun<int>(int t)
{
cout<<t<<endl;
}
调用顺序:原版函数->具体化->模板
实例化过程
1.模板解析:编译器分析模板函数定义,根据提供的类型参数生成代码。
2.类型替换:模板参数T被实际类型替换,生成适用类型的代码。
3.函数生成:编译器实现函数,生成可执行二进制文件。
类模板
template <typename T>
class A
{
public:
T val;
void show(T t);
void getval(T t) {val = t;}
void A(const A<T> &a) : val(a.val) {}
//A<T>与A<U>,T代表同类型,U代表不同类型
}
template <typename T>
void A<T>::show()
{
val = t;
cout<<val<<endl;
}
template <typename T>
class B : public A<T>
{
T
}
int main()
{
A* a1 = new A<int>;
A<int> a2;
}
多态模板
A<short, int>* a = new B;
//子类没模板
A<short, int>* a1 = new B<short, int>;
//子类有模板,类型要对上
常用STL
明天写
版权归原作者 m0_67582670 所有, 如有侵权,请联系我们删除。