我的小站——半生瓜のblog
@TOC
namespace_命名空间
C++避免名字冲突——使用命名空间。
例如:
不同命名空间中的同一个变量,所对应的内容不同。
#include<iostream>
namespace China {
double population = 14.1;
std::string capital = "北京";
}
namespace XiaoRiben {
double population = 1.27;
std::string capital = "东京";
}
int main(void)
{
std::cout << China::capital << std::endl;
std::cout << XiaoRiben::capital << std::endl;
return 0;
}
cout是标准命名空间std已经定义好的。
或者用using namespace xxx;使用对应的命名空间。
就算using namespace xxx了一个命名空间,我们仍然可以通过xxx::来使用其它的命名空间。
或者只指定使用某个命名空间中的一个变量,using namespace China::capital;
之后出现的capital都是China这个命名空间对应的数据。
software_生成过程
源程序.cpp——》预处理——》源程序a.cpp——》编译——》汇编程序a.s——》汇编——》二进制程序a.o——》链接+C++库文件——》可执行文件.exe
VS智能补全ctrl+j
data type_数据类型
不同的数据类型
- 表示的意义不同
- 占用内存不同
- 表示的范围不同
- 使用方法不同
variable_变量
无符号整型,对应的范围是其有符号的两倍,0~有符号对应的两倍.
unsigned int 可以简写成unsigned
无符号数不能表示负数。
如果强行用无符号数表示负数,实际存储的是这个负数对应的补码。
即该负数 + “对应类型的模值(最大值)
16位系统中一个int能存储的数据的范围为-32768~32767,而unsigned int能存储的数据范围则是0~65535,在计算机中,整数是以补码形式存放的。 无符号整型和有符号整型运算依据表示数据的最大值来定,二者数据运算先将有符号整型转换成无符号整型再通过无符号数运算规则来计算。
注意:1 和 '1'的区别,'1'对应的ASCII码值是49
float在内存中的存储方式-符号位-阶码-尾数
阶码——指数+127
符号位——尾数 * 2^(阶码-127)
double在内存中的存储方式与float类似
带小数的默认常量都是double类型。
3.14f——强制指定是float类型
可以用科学计数法来表示浮点型的常量:
1.75E5 or 1.75 e5
就是1.75成10的5次方。
cout
默认输出6位。
修改精度——cout.precision(精度-小数点前后都算上);
修改小数点后位数——cout.flags(cout.fixed);定点法
取消顶点法——cout.unsetf(cout.fixed)
C语言清空输入缓冲区——fflush(stdin);
cin
当输入缓冲区为空时,程序才会暂停,让用户输入数据。
输入回车之后,数据全部送到输入缓冲区。
输入数据时,前面的数据输入错误,导致后面的数据都不能输入。
ctrl+z——文件结束符
输入判断:
if(cin.fail())
{
cout<<"提示"<<endl;
cin.clear();//清除cin的错误标志
cin.sync();//清空输入缓冲区
}
getch()
cmd窗口没有回显。
constant_常量
几进制每一位就有几种可能
string
遇到空格 回车 文件结束符结束。
字符串的比较 ——本质是字符串中字符的比较
例如:
"19" > "123"
"1230" > "123"
下标方式也可以访问string字符串中的字符。
注意:C风格的不同字符串赋值方式,是否需要显示 指定/0不同。
拓展
getline
getline(cin,addr);
//从标准输入设备cin,读取一行字符串,保存到字符串变量addr中,
//直到遇到回车符,不包括回车符。
array
数组中的各个成员时连续存储在内存中的。8个依次相连的邻居。
没指定数值,就是0。
(我发现数组并不能重新对数组整体以{}的形式重新赋值。)
特殊写法——定义时仅指定部分成员。(乱序方式)。C编译器支持
int temp[10] = { [4] = 2,[5] = 3 ,[3] = 3};
Bit operation_位运算
内存的基本单位是字节,一个字节8个比特位。
位与& 位或| 位非~
左移<< 右移>>
向左移动n个位置,就等同于乘以2的n次方
右移相反。
1和0与1进行按位与结果都是1
1和0与0进行按位与结果都是0
如何将一个数的最后四位变成0110?
要先将一个数的最后四位变成0,就将这个数与~15进行按位与运算
,然后再与6进行按位或运算。
int a = 0;
cin >> a;
a = a & (~15);
a = a | 6;
cout << a;
priority_优先级
comma expression_逗号表达式
逗号表达式的优先级最低
a = 4 * 5, 3 + 5,10/2;//20
a = (4 * 5, 3 + 5,10/2);//5
Cast type _强制类型转换
超出的ASCII码——链接
switch
loop_循环
循环次数已经确定的情况,用for循环更方便。
do while——先执行一次,然后根据条件判断,是否进行下一轮循环。
简单思路提供——将一个问题拆分成多个小问题
头文件
防卫式声明,防止头文件重复包含
#ifndef xxx
#define xxx
#endif
vs独有
#pragma once
function
默认参数
C语言不支持默认参数
默认参数要写到其他参数后面。**
函数重载
C++可以实现使用同名函数【函数重载】来实现功能类似的多个不同函数。
区分不同函数
1.参数个数不用,2.参数类型不同
(与返回值类型无关)
function stack space_函数栈空间
每个函数都会在栈空间中分配到一块内存来给它使用。
这块内存区域就叫做栈帧。
当定义的数组过大,超过了栈空间的大小时, 访问它最后的位置,程序就会崩掉。
inline
缺点:使用内联函数的程序,会变得"臃肿",消耗调用函数的栈空间。
使用场合:内联函数的使用场合应该是十分简单,执行很快的几条语句。
这个函数的使用频率非常高,比如在一个循环中被使用千万次的使用。
recursion_递归
在函数内部 ,直接或间接调用自己。
一定要定义一个结束条件。
逐个返回到函数的调用处。
Static library_静态库
windows桌面项目——》lib——》生成解决方案
array_数组
为了提高可读性,尽量不要让编译器自动推导。
越简单的越好——软件工程
defence code_防御式编程
保证我们能对出现的错误进行处理
- 对输入进行体检
- 对非预期错误使用断言]
#include<arssert.h>
pointer
建议初始化指针为NULL,避免访问非法数据。
不同类型的指针,所对应的步长不同。
区分
const int* xxx;——不能更改对应地址的值(指向常量的指针)
int const* xxx;——不能更改对应地址的值,同上
int* const xxx;——不能更改指向的地址(常量指针)
const int* const xxx;——都不能修改
const在谁后面就不能修改谁。
二级指针——什么时候要传
理解:指针的地址要用二级指针来存。
直接使用二级指针名字,得到的是它里面存的一级指针的地址。
就像直接使用一级指针名字,得到的是它里面存的变量的地址一样。
指针要一级一级指向。
void temp(int** p1)
{
static int boy = 25;
*p1 = &boy;
}
int main(void)
{
int* pTemp = NULL;
temp(&pTemp);
cout << *pTemp;
}
指向多维数组的指针
数组
int arrt[4][3];
int (*p)[3];
int a[2][3] = { {1,2,3},{4,5,6} };
int (*p)[3] = NULL;
p = a;
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 3; j++)
{
cout << (*(p+i))[j] << " ";
}
cout << endl;
}
指向数组的指针就是二级指针。
传递二级指针
因为传递的是指针数组的名字,传递过来的就是这个指针数组的首元素的地址,它的首元素有同样是个指针, 所以用 二级指针接收。
void test(int **p1)
{
}
int main(void)
{
int a[2][3] = { {1,2,3},{4,5,6} };
int *p[3] = {NULL};
test(p);
return 0;
}
注意
int *p[2];是指针数组,里面存的是地址(指针)
int (*p)[2];是数组指针,指向一个有2个变量的指针。
对比记忆
#include<iostream>
#include<string>
#include<Windows.h>
using namespace std;
void test(int **p1)
{
}
int main(void)
{
//数组指针——指向数组——存放的是一个数组的地址
int a[2][3] = { {1,2,3},{4,5,6}};
int(*p)[3] = a;
cout << p << endl;
cout << *(p[0]+1) << endl;//2
cout << *(p[1]) << endl;//4
//指针数组——存放指针
int a2[3] = { 0,1 };
int* p2[2];
p2[0] = a2;
cout << &a2 << endl;
cout << *(p2[0]+1);
//传递二级指针的是指针数组——存放指针才是指针的指针
test(p2);
return 0;
}
void指针不允许进行加减运算
其它类型的指针都可以隐式自动转换成void类型指针,反之则需要强制转换。
int main(void)
{
int arr[] = {1,2,3};
void* p = arr;
char a = 'a';
p = &a;
char* p1 = (char *)p;
cout << *p1;
return 0;
}
(释放时指针的位置要和原来创建时候指向的位置相同。)
function_pointer
#include<iostream>
#include<string>
#include<Windows.h>
using namespace std;
void test()
{
cout << "wuhu" << endl;
}
int main(void)
{
void (*fc)();//与上面函数声明的返回值个参数类型一致
fc = &test;
//两种调用方式
(*fc)();
fc();
return 0;
}
reference_引用
已有变量名的别名,操作的是这个名字所对应内存的数据。
1.本质——C++编译器到底在背后做了什么?
int &b = a ;——》 int* const b = &a;
2.引用有没有自己的空间?
引用是有自己的空间的。
3.同一内存空间可以取多个别名
(当我们使用一些引用语法的时候,无需关心编译器背后是如何作用的,但当我们分析奇怪语法现象的时候,我们才会关系C++编译器是怎么去做的)
pointer reference
int a = 10;
int* p = &a;
int*& q = p;
cout << *q;
可以代替二级指针。
constant reference
const Type& name;
只能通过这个别名读这个变量的值,不能去修改它。
用字面常量初始化常量引用——没有意义
const int& b = 10;
int c = b;
file_operator
file>>形式读取文件,会自动换行。
memory branch
stack:由编译器自动分配释放,存放函数的参数值,局部变量等。
heap:由coder分配释放。
static:全局变量和静态变量是存放在一起的,在程序编译时分配。当一个变量前加上static后,加定它在一个函数中,那么在这个函数 执行完毕之后 ,它的变量并不会变回初始化的那个值,而是变成了当前函数执行完毕后,该变量变成的值,并且该变量在函数外部无法访问。如果定义为了全局静态变量,则在程序范围内都可以访问到。(只局限于这个源文件)
文字常量区:存放常量字符串。
程序代码区:存放函数体(包括类的成员函数、全局函数)的二进制代码。
dynamic momory
内存拷贝函数:
void* memcpy(void* dest,const void* src,size_t n);
从原来的scr所指向的内存的起始位置,拷贝n个字节到目标dest所指的内存起始位置中。
注意:目的地要new出来大小。
提示:
- 可以输入多少就new多少空间
- malloc开辟内存得到的指针是void*的
- 64位win10 heap限制是2G,根本就不用担心,因为我们使用不到这么多内存。
- C++保留mallo和free为了向下兼容
- 基础类型malloc,new,delete,free可以混搭
开辟空间并初始化
int* sb = new int(100);
cout << *sb;
Variable storage mode
寄存器变量——
register:C++中的register已经优化,如果打印register变量的地址,编译器会自动降级。
不能定义成全局变量。
auto——
注意看 C++的特性
static——
静态,静态变量只能被初始化一次。
exterb——
比static更全局,A.cpp中的可以在B.cpp中使用。
实际使用中,定义到.cpp文件中,否则定义到.h文件中,可能会有多个全局变量了。
define
- 提高代码可读性
- 提高程序效率
struct
结构体变量作为参数,传值是值传递。
enum
同一类型变量的几种可能。(int)
将很多define集成到一起。
从0开始,逐渐递增,第二个元素在前面的基础上+1;
调用自动++,切换下一个元素。
OPP&OOP
面向过程编程OPP:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注“怎么做”,即完成任务的具体细节。
面向对象编程OOP:Object Oriented Programming,是一种以对象为基础的编程思想。主要关注“谁来做”,即完成任务的对象。
面向过程:
根据程序的执行过程来设计软件的所有细节。
缺点:
开发大型项目时,越来越难把控,甚至失去控制。
面向对象:
大型项目必备。
class
什么是对象?
特定类的具体实例。
对象和普通的变量有什么区别?
一个对象就是一个特殊变量,但是有丰富的功能和用法。
Constructor
构造函数也可以重载。
种类:
- 默认构造函数
一般当数据成员全部使用了“类内初始值”,才使用"合成的默认构造函数"。
- 自定义的构造函数
- 拷贝构造函数
深浅拷贝。
调用时机——函数传参,不是引用方式。函数返回类型是类,而且不是引用类型。对象数组的初始化列表中,使用对象。
- 赋值构造函数
指针——一定要记住开辟空间
new了一定要delete,尽量开辟大一点的空间。
返回值要注意连用情况。
简单的实例:
#include<iostream>
#include<string>
using namespace std;
class Human
{
public:
Human()
{
name = "未知的";
addr = new char[64];
strcpy(addr, "未知的");
}
Human(const string namedtor,const char*addrdtor)
{
name = namedtor;
addr = new char[64];
strcpy(addr, addrdtor);
}
~Human()
{
delete[] addr;
}
void GetAddr()const
{
cout << this->name << " ";
cout << this->addr << endl;
}
void ChangeAddr(const char* tempChange)
{
strcpy(addr, tempChange);
}
Human& operator=(const Human& other)
{
if (&other == this)
{
return *this;
}
name = other.name;
strcpy(addr, other.addr);
}
Human(const Human& other)
{
this->name = other.name;
addr = new char[64];
strcpy(this->addr, other.addr);
}
private:
string name;
char* addr;
};
int main(void)
{
Human h1("zhang", "China");
h1.GetAddr();
h1.ChangeAddr("japan");
h1.GetAddr();
Human h2(h1);
h2.GetAddr();
Human h3;
h3 = h1;
h3.GetAddr();
return 0;
}
static class members
private:
static int count ;
.....
对类的静态成员进行初始化
int Human::count =0;
加了const可以直接在类内初始化
所有的成员函数都可以访问静态数据成员。
不能通过类名访问静态数据成员
static class function members
类的静态成员函数。
例如上面静态成员计算一个数量。
直接用类调用他的函数即可。
对象可以直接访问静态成员函数。
在类的静态成员函数内部不能直接访问this指针和对象的数据成员。
只能访问类的静态数据成员。
const class member
C++11可以在类内给const修饰的变量赋值。
或者
在类构造函数的初始化列表赋值。
在构造函数中初始化。
const class function member
const修饰的成员函数不能修改任何数据成员。
const修饰的对象只能调用const的成员函数。
一个小问题——
const修饰的对象,能否修改其类内数据?
答案是不能,const修饰的对象只能调用const的成员函数,修改对象数据的内类函数,肯定不能加const修饰,所以它不能修改其类数据。
不改变对象数据的成员函数,+const来修饰。
combination_组合
polymerization_聚合
最好头文件不包含头文件
聚合不是组成关系,被包含的对象,也可能被其他对象包含。
拥有者,不需要对被拥有的对象的声明周期负责。
代码示例:
#include<iostream>
#include"Computer.h"
#include"Cpu.h"
#include"VoiceBox.h"
using namespace std;
void ceshi(VoiceBox* box)
{
Computer MyComputer("intel", "i9", 512, 16);
MyComputer.addVoiceBox(box);
}
int main(void)
{
VoiceBox box;
ceshi(&box);
//加了这一行pause说明程序还没执行完,电脑被销毁了,但是它的音响还在,我拔下来就能插到其他地方去。
//之前显示音响被销毁,是因为程序执行完了。
system("pause");
return 0;
}
summary_1
常量最好定义成宏
构造函数初始化列表,写在定义中
再次强调,const对象只能调用const方法
vector——push_back,将值拷贝过去,实际上是两个东西,拷贝了 一份。
const修饰的对象传引用时,起的别名也要是const修饰的
非const修饰的对象,可以传递到const修饰的引用参数
静态方法里面只能调用静态方法以及该类的静态成员
非static方法叫做——实例方法
从现实生活中把握C++——模拟现实
Inheritance and Derive_继承与派生
父亲——派生——儿子
儿子——继承——父亲
继承和派生本质上是相同的,只是从不同的角度出发。
父类的所有成员函数以及数据成员都会被子类继承
先调用父类的构造函数,再调用子类的构造函数。
先调用父类的构造函数用来初始化从父类继承来的数据。
再调用自己的构造函数,用来初始化自己定义的数据。
没有体现父类的构造函数 ,就会自动调用父类的默认构造函数。
子类想要访问父类的数据:
- 将父类成员数据改成——protected属性
- 通过继承父类中的成员函数来的获得。
子类中有父类中相同的成员函数,优先调用子类自己的,找不到再去父类里面找,还找不到,那就失败。
成员函数,不占用对象的内存空间,但是也被子类继承了。
先分布从父类继承的数据成员,再分布子类自己定义的数据成员。
Access rights_访问权限
public:
外部可以直接访问,可以通过对象来访问这个成员。
private:
外部不可以访问,类内访问。
protected:
和private十分相似,唯一区别,如下所示:
如果在设计类的时候,父类的成员,希望它的成员希望,可以被自己的子类派生类直接访问,但是又不想被外部直接访问,那就可以把这些成员定义为protected。
Inheritance ways_继承方式
public:
完全继承父类,父类的成员,之前是什么属性的,继承过来还是什么属性的。访问权限。
private:
继承过来,访问权限都变成private。
protected:
public变protected,其他不变。
继承方式的不同,影响外部通过子类访问父类成员。
调用父类的构造函数:
- 在子类的构造函数中 ,显式调用父类的构造函数。(例如:初始值列表)
- 没有显示调用父类构造函数,那么会自动调用父类的默认构造函数。
调用顺序:
静态类数据成员的构造函数——》父类的构造函数——》非静态数据成员的构造函数——》自己的构造函数。
(类的静态成员)静态对象只创建一次。(构造函数、析构函数只执行一次。)
当子类以public方式继承父类时,子类的对象可以代替父类对象处理。
即:形参为基类对象时,实参可以是派生类对象。
反过来父类不能代替子类。
子类型关系具有单项传递性:
C类是B类的子类型, B类是A类的子类型,所以C类是A类的子类型。
子类型的应用:
- 父类指针可以指向子类对象(配合多态实现子类的方法)
- 子类对象可以初始化基类引用(效果同上)
- 父类的对象可以被赋值为子类对象
multiple inheritance_多重继承
多继承/多重继承:
一个派生类可以有两个或多个积累。
多重继承在中小型项目中较少使用。
multiple inheritance Ambiguity_多重继承二义性
多个父类中有相同成员函数,子类调用时加上类名来区分。指定基类来使用。
子类.父类::方法();
或者子类重新定义这个方法,在里面使用基类名进行限定来调用对应的基类的方法。
一个类有两个子类, 这两个子类又是一个子类的父类。
容易产生二义性。共同的基类被继承,有两份数据,产生访问歧义。
解决方案——使用虚继承
virtual public
相同的数据只取一份
file operate
iostream-input-output
C++的IO流:向设备写数据/从设备读数据
设备:文件、控制台、特定的数据类型(stringstream)。
open ways
模式标志 | 描述 |
---|---|
ios::in | 读方式打开文件 |
ios::out | 写方式打开文件 |
ios::trunc | 如果此文件已经存在,就会在打开文件之前把文件长度截断为0 |
ios::app | 尾部最佳方式(在尾部写入) |
ios::ate | 文件打开后,定位到文件尾 |
ios::binary | 二进制方式(是文本方式) |
以上打开方式可以用 | 组合起来使用。
文本文件和二进制文件
区别:
文本文件——写数字1,实际写入的是'1'
二进制文件——写数字1,实际写入的是整数1(0001)
写字符'R',实际输入的还是'R'
二进制读写——C++ read()和write()读写二进制文件(超级详细)
按指定格式读写数据
按指定格式写文件
#include<iostream>
#include<string>
#include<fstream>
#include<sstream>
using namespace std;
int main(void)
{
string name;
int age;
ofstream ofs;
ofs.open("user.txt");
while (1)
{
cin >> name;
if (cin.eof())
{
break;
}
cin >> age;
stringstream s;
s << name << " " << age << " " << endl;
ofs << s.str();
}
return 0;
}
指定格式读文件
#include<iostream>
#include<string>
#include<fstream>
using namespace std;
int main(void)
{
string line;
int age;
char name[20];
ifstream ifs;
ifs.open("user.txt");
while (1)
{
getline(ifs, line);
if (ifs.eof())
{
break;
}
sscanf_s(line.c_str(), "姓名:%s 年龄:%d ", name, sizeof(name), &age);
cout << name << '\t\t\t' << age << endl;
}
ifs.close();
return 0;
}
seekg——设置输入流的位置
tellg——返回该输入流的当前位置(距离文件起始位置的偏移量)
seekp——设置该输出流的位置
提高代码的健壮性和可读性,宏定义可以解决很多麻烦,名称写死,在大型项目中可能是致命的。
cin.ignore(count, c);
从输入流中提取并丢弃字符,直到遇到下列三种情况
1.提取的字符达到了参数count指定的数量
2.在输入序列中遇到文件结束(EOF)
3.输入序列中的下一个字符为参数c指定的字符(这个字符会被提取并丢弃)
count常常取:
std::numeric_limits< std::streamsize >::max() 相当于IO流的最大字符个数
常见用法:(把标准输入缓冲区cin的所有数据都清空)
cin.ignore(std::numeric_limits::max(), '\n');
从文中读取数据进行大小的比较,可以先读取一个数字,然后把各项数值都设为它,然后一个一个的往下读。
friend function
某个类需要实现某种功能,但是这个类自身因为某种原因,无法自己实现,需要借助“外力”才能实现。
全局函数做友元函数
一个类的成员函数作为另外一个类的友元函数
C++开发中,能不用全局函数就不用全局函数。
尽可能的用类的概念来做。
frend class
这个类都是友元,这里面的所有成员函数都可以访问另一个类里面的私有成员。
就相当与把这个类里面的所有成员函数都声明为另一个类的友元函数。
友元类、友元函数,使用friend关键字进行声明即可,与访问权限无关。
放到private、protect、public任意区域内
Operator overloading
- 使用成员函数进行运算符重载
- 使用非成员函数进行运算符重载
两种方式的选择:
- 一般情况下,单目运算符,使用成员函数重载更方便(不用写参数)。
- 一般情况下,双目运算符,使用友元函数更直观。
例如:
100 + person 只能通过友元来实现。
person + 100友元函数和成员函数都可以实现。
注意:
C++规定运算符重载的操作对象至少有一个不是标准类型,而是用户自定义的类型。
特殊情况:
(1)= () [] -> 不能重载为类的友元函数。否则可能和C++的其他规则矛盾,只能使用成员函数形式进行重载。
(2)如果运算符的第一个操作数要求使用隐式类型转换,则必须为友元函数(成员函数方式的第一个参数是this指针)
如果新得到的结果放到了已经有的空间位置上,就OK。——返回引用
注意重载赋值运算符的连用情况。
[]也可以重载,宏定义(枚举)使得程序更加健壮,尽可能的不要在代码中写裸常量。
最好在编写代码的时候让编译器来帮我们找出错误,这样能减少很多麻烦。
相比与C风格的字符串,string风格的字符串更加优雅。
string中的c_str(),返回一个指向与本字符串内容相同的char类型指针。
类型转换函数——operatoir type()const
类类型转普通类型
例如:
Boy boy1("王小花",15);
int a = boy1;
operator int()const
常量类型调常量方法。——const对象只能调const方法。如果找不到合适的const方法就会出问题。
类类型转类类型:
- 调用对应的只有一个参数的构造函数
- 也可以使用类型转换函数
使用对应的构造函数更合适。
构造函数的参数列表的初值,只在类内声明的时候写,如果定义和声明都写了,就会报错——重定义默认参数。
polymorphism
多态的本质:
形式上,使用统一的父类指针做一般性处理,但是实际执行时,这个指针可以指向子类对象,形式上,原本调用父类的方法,但是实际上会调用子类的同名方法。
注意:
程序执行时,父类指针指向父类对象,或子类对象的时候,在形式上是无法分辨的!
只有通过多态机制,才能执行真正对应的方法。
virtual function
(这个小标题的内容已再上一篇文章单独出)
(补充:函数指针的概念)——链接
例如:
double (*ps)(int);
ps指针指向的函数,返回值是doble 参数是int
注意:
- 在函数声明的返回类型之前加virtual。
- 并且只在函数的声明中添加virtual,在该成员函数的实现中不用加。
虚函数的继承
- 如果某个成员函数被声明成虚函数,那么他的子类,以及子类中的子类 ,所计继承的这个成员函数,也自动是虚函数。
- 如果在子类中重写这个虚函数,可以不用再加virtual,但仍然建议加上virtual,提高代码的可读性。
虚函数原理——虚函数表
对应虚函数的类,该类的对象所占内存大小为,数据成员的大小+一个指向虚函数表指针 (4字节)。
例如:如下所示Father类所创建的对象
class Father
{
public:
virtual void func1()
{
cout << "虚函数func1" << endl;
}
virtual void func2()
{
cout << "虚函数func2" << endl;
}
virtual void func3()
{
cout << "虚函数func3" << endl;
}
void func4()
{
cout << "非func4" << endl;
}
public:
int x = 200;
int y = 300;
static int z;
};
int Father::z = 0;
Father father;cout<<sizeof(father)<<endl;
结果为12,两个int的数据成员4+4一共占了8个字节,再加上一个虚函数表指针(4个字节),一共是12个字节
( 如果该类中没有虚函数,就没有虚函数表指针,也就少4个字节)
如下图所示:
思考:它尽然是个指针,那我们就能通过这个指针来访问它所指向内存所对应的内容。
(先存的是虚函数表指针,然后才是数据成员。)
所以说,对象地址就是虚函数表地址。
cout<<(int*)&father<<endl;
强转成指针。
接着,取出虚函数表的指针。
int* vptr = (int*)*(int*)(&father);
为了编译器能通过,前面加上int*。
然后,就找到了虚函数,并执行方法。
为了便于调用,这里定义个函数指针类型。
typedef void(*func_t)(void);
func_t指针,指向参数为void,返回值为void的函数。
调用虚函数。
((func_t)*(vptr))();
((func_t)*(vptr + 1))();
((func_t)*(vptr + 2))();
调用成功。
接着调用x,y两个数据成员。
cout << *(int*)((int)&father+ 4) << endl;
cout << *(int*)((int)&father+ 8) << endl;
取到地址,转成int整数,加上偏移量,通过编译器加上(int*),再解引用,得到里面的值。
(+上偏移量要先转成int)
多态的使用:父类指针指向子类对象
Father* father1 = &son;
father1->Func1();//调用对应的func1函数,son中的
使用继承的虚函数表
在上面的基础上,为Father类添加一个派生类。并且对Father的func1进行重写,再添加一个它独有的func5,声明为虚函数。
class Son :public Father
{
public:
virtual void Func1()
{
cout << "Son Func1()" << endl;
}
virtual void Func5()
{
cout << "Son Func5" << endl;
}
};
同上面通过使用指向虚函数表的指针来访问对应的内容
for (int i = 0; i < 4; i++)
{
//取到这个地址的内容,然后通过自定义指针类型转换,调用该函数,加()
((func_t) * (vptr + i))();
}
// 访问两个成员
cout << *(int*)((int)&son + 4) << endl;
cout << *(int*)((int)&son + 8) << endl;
子类虚函数表
- 直接复制父类的虚函数表
- 如果子类重写了父类的某个虚函数,那么就在这个虚函数表中进行相应的替换
- 如果子类中添加的新的虚函数,就把这个虚函数添加到虚函数表中(尾部添加)
" title="">
使用多重继承的虚函数表
在上面的基础上再添加一个Mother类
class Mother
{
public:
virtual void handle1()
{
cout << "Monther handle1" << endl;
}
virtual void handle2()
{
cout << "Monther handle2" << endl;
}
virtual void handle3()
{
cout << "Monther handle3" << endl;
}
public://便于测试,所以权限定为public
int m = 400;
int n = 500;
此时的Son类对象
vs编译器中把子类自己的虚函数放到了第一个父类的虚函数表最后
同样通过指针访问对应的虚函数表内容
Son son;
cout << (int*)&son << endl;
//第一个虚函数表指针
int* vptr1 = (int*)*(int*)&son;
for (int i = 0; i < 4; i++)
{
((func_t)*(vptr1 + i))();
}
// x y
for (int i = 0; i < 2; i++)
{
cout << *(int*)((int)&son + 4 + 4 * i) << endl;
}
//第二盒个虚函数表指针
int* vptr2 = (int*)*((int*)&son + 3);//取出来的是指向第二个虚函数表的指针
for (int i = 0; i < 3; i++)
{
((func_t)*(vptr2 + i))();
}
//m n
for (int i = 0; i < 2; i++)
{
cout << *(int*)((int)&son + 16 + i * 4) << endl;
}
小补充:
对象地址+偏移量
转化int类型 + 对应的字节个数
转化int*类型 + 走几步(几个步长)
虚函数的修饰
final
final——C++11更新
1.用来修饰类,让该类不能被继承。
class XiaoMi{};
class XiaoMi2 final:XiaoMi{};
class XiaoMi3 :XiaoMi3//报错——XiaoMI2不能被继承{};
(补充:C++默认继承方式为private)
2.用来修饰虚函数,使得该虚函数在子类中,不得被重写。但是还可以使用。
override
override仅能修饰虚函数。
只能用在函数的声明,函数的实现不要写。
作用:
- 提示程序的阅读者,这个函数是重写父类的功能。
- 防止程序员在重写父类的函数时,把函数名写错。
父类的虚析构函数
把father类的析构函数定义为virtual时,并且对父类的指针执行delete操作时, 就是对该指针使用"动态析构"。
如果这个指针指向的是子类对象,那么会先调用该子类的析构函数,再调用父类的析构函数。
如果指向的是父类对象,那么只调用父类的析构函数。
注意:
为了防止内存泄露,最好在基类的虚构函数上添加virtual关键字,使基类析构函数为虚函数。
纯虚函数与抽象类
什么时候使用纯虚函数?
某些类,现实项目和实现角度吗,都不需要实例化(不需要创建它的对象)。
这个类中定义的某些成员函数只是为了提供一个形式上的接口,准备让自子类来做具体的实现。
此时这个函数就可以定义为"纯虚函数",包含纯虚函数的类,就叫做抽象类(不能创建对象)。
继承该抽象类的子类如果不重写这个纯虚函数,那么它也是不能创建对象的。
用法:
virtual + = 0
代码示例:
#include<iostream>
#include<string>
using namespace std;
class Shape
{
public:
Shape(const string& color = "White")
{
this->color = color;
}
virtual float area() = 0;
~Shape()
{
}
private:
string color;
};
class Circle :public Shape
{
public:
Circle(float radius = 0, const string& color = "White") :Shape(color), r(radius)
{
}
virtual float area()
{
return 3.14 * r * r;
}
~Circle()
{
}
private:
float r;
};
int main(void)
{
Circle c1(3);
cout << c1.area() << endl;
return 0;
}
纯虚函数的注意事项:
父类声明为某纯虚函数之后,它的子类:
- 实现这个纯虚函数
- 继续把这个纯虚函数声明为纯虚函数,这个子类也称为抽象类
- 不对这个纯虚函数做任何处理,等效于上一种情况(不推荐)
重复提示注意——代码尽量不要写裸常量