目录
(1)概念
封装:把函数定义到结构体内部,函数可以使用结构体的其他成员
类:带有函数的结构体
成员函数:结构体里的函数,不占用结构体的内存空间
//成员函数不占用结构体的内存空间,因为它本不属于这个结构体
struct std{
int x; int y; int m; int n;
int plus()
{
return x+y;
}
};
int main()
{
int r = sizeof(a); //输出结果r为16
std a = {1,2,3,4};
mov dword ptr [ebp-10h],1 //结构体首地址
mov dword ptr [ebp-0Ch],2
mov dword ptr [ebp-8h],3
mov dword ptr [ebp-4h],4
a.plus();
lea ecx,[ebp-10h] //虽然函数无参数,编译器把this指针传给了函数
call plus
return 0;
}
(2)this指针
- this指针:编译器把结构体的首地址作为参数传入成员函数
- this指针是编译器默认传入的,通常使用ecx进行参数的传递
- 成员函数都有this指针,无论是否使用,成员函数可以使用this指针来调用其他成员
- this指针不能做++,--等运算,不能被重新赋值,this指针就是结构体首地址
- this指针不占用结构体的宽度
(3)构造函数、析构函数
struct StdClass
{
int x; int y; int m; int n;
StdClass() //编译器不要求一定要有构造函数
{
printf("与类名相同的函数,称为构造函数");
printf("无返回值,void也不行");
printf("构造函数可以有参数,也可以没有");
printf("构造函数可以有多个,称为重载");
}
~StdClass() //编译器不要求一定要有析构函数
{
printf("和构造函数一样,无返回值");
printf("析构函数只能有一个,不能重载");
printf("不能有参数");
}
};
int main()
{
StdClass a; //创建一个对象,构造函数会自动执行,主要用于初始化
return 0; //销毁对象时,析构函数执行,主要用于清理工作
}
析构函数何时执行:
- 当对象在堆栈中分配(局部变量)
在main函数中创建对象,当main函数执行完毕,在return返回之前,析构函数会执行
- 当对象在全局区分配(全局变量)
在应用程序(进程)退出之前,析构函数会执行
(4)继承
1)什么是继承
//继承就是数据的复制,减少重复代码的编写
struct person
{
int age; int sex; //person是父类、基类
};
struct teacher
{
int age; int sex; //未使用继承,则要重复编写代码
int level; int classid;
};
struct teacher:person //teacher是子类、派生类,teacher继承了person的代码
{
int level; int classid;
};
2)父类与子类有相同成员
struct person
{
int age; int sex;
};
struct teacher:person
{
int age; int classid;
};
int main()
{
teacher t;
int x = sizeof(t); //输出结果x为16,说明只要继承,编译器就会帮我们重写代码
t.age = 1; //内存[ebp-10h]未赋值,说明编译器默认使用子类成员
mov dword ptr [ebp-8],1
t.sex = 2;
mov dword ptr [ebp-0Ch],2
t.classid = 3;
mov dword ptr [ebp-4],3
t.person::age = 1; //要使用父类的成员
mov dword ptr [ebp-10h],1
return 0;
}
3)多重继承
//继承方式为空,默认为private
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>
{
<派生类类体>
};
struct x{int a; int b;};
struct y{int c; int d;};
struct z:x,y //z同时继承了x、y,x和y的前后顺序决定了成员在内存中的顺序
{
int e; int f;
};
int m = sizeof(z); //输出结果为24
(5)类成员的访问控制
1)定义与实现分开
把定义写在头文件中,实现写在源文件中,是良好的编程习惯
2)public和private的使用
- 结构体内部的函数才能访问private成员
- public / private可以修饰函数和变量
- 通过指针访问私有成员
struct test{
private:
int x;
public:
int y;
void init(int x,int y)
{
this->x = x; this->y = y;
}
};
test.t;
t.init(1,2);
int* p = (int*)&t;
printf("%d %d",*p,*(p+1)); //输出结果为1,2
3)class与struct的区别
成员访问区别:class默认类型为私有,struct默认类型为公有
4)继承中的访问控制
- private继承方式,会把父类中的公有成员转成私有成员
- 父类中的私有成员会被继承,子类不能直接访问父类私有成员
(6)在堆中创建对象
1)我们可以在以下3处创建对象
class person
{
private:
int x; int y;
pubilc:
person()
{printf("person()执行");}
person(int x,int y)
{
printf("person(int x,int y)执行");
this->x = x; this->y = y;
}
~person()
{printf("~person()执行");}
};
person p; //在全局变量区
void max() //在栈中
{
person p;
}
int main()
{
person* p = new person(1,2); //在堆中创建对象,并调用构造函数
delete p; //释放对象占用的内存,并调用析构函数
return 0;
}
2)C语言的内存分配和释放
//C语言库函数
malloc(n); //在堆中分配一块内存
free; //释放堆中内存空间
new 相当于 malloc + 构造函数
delete 相当于 free + 析构函数
3)new和delete
- 用C和C++的方式在堆中申请int数组
int* p = (int*)malloc(sizeof(int)*10); free(p);
int* p = new int[10]; delete[] p;
- 用C和C++的方式在堆中申请Class类型数组
int* p = (person*)malloc(sizeof(person)*10); //malloc仅仅分配为对象分配空间
free(p); //仅仅释放空间
int* p = new person[10]; //new为10个对象分配空间,并调用10次析构函数
delete[] p; //释放空间,并执行10次析构函数
delete p; //只释放一个对象空间,执行一次析构函数
- 创建一个对象用:new 和 delete
- 创建一个对象数组用:new [ ] 和 delete [ ]
(7)引用类型
1)引用是变量的别名
引用类型的变量定义时必须赋初值,只能指向一个变量
class base{
pubilc:
int x;
};
int main()
{
//基本类型
int a = 10;
int& b = a;
printf("%d",x);
//类
base c;
c.x = 1;
base& d = c;
//指针类型
int*** e = (int***)1;
int***& f = e;
//数组
int arr[] = {1,2,3};
int (&p)[3] = arr; //此时p是数组的别名
p[0] = 4;
printf("%d",arr[0]); //输出结果为4
return 0;
}
2)引用类型与指针的区别
int x = 1; //必须初始化
int* p = &x;
int& a = x;
//运算
p++;
mov edx,dword ptr [ebp-8]
add edx,4
mov dword ptr [ebp-8],edx
a++; //相当于x++
mov eax,dword ptr [ebp-0Ch] //[ebp-0Ch]就是x的地址
mov ecx,dword ptr [eax]
add ecx,1
mov edx,dword ptr [ebp-0Ch]
mov dword ptr [edx],ecx
//赋值
p = (int*)1;
a = 100;
3)引用在函数参数传递中的作用
- 基本类型
void plus(int& i)
{
i++;return;
}
int main()
{
int i = 10;
plus(i);
lea eax,[ebp-4]
push eax
call plus
add esp,4
printf("%d",i);
return 0;
}
- 构造类型
struct base
{
int x; int y;
base(int x,int y)
{
this->x = x; this->y = y;
}
};
void PrintByRef(base& ref,base* p)
{
printf("%d %d",p->x,p->y); //通过指针读取
printf("%d %d",ref.x,ref.y); //通过引用读取
//指针可以进行赋值、运算
}
4)常引用
防止指针对变量进行错误修改
class base
{
public:int x;
};
void print(const base& ref)
{
//ref = 100; 不能修改
//ref.x = 200; 可以修改指向的内容
printf("%d",ref.x);
}
int main()
{
base b; b.x = 100;
print(b);
return 0;
}
(8)面向对象程序设计
1)继承、封装
- 子类继承父类成员变量、成员函数
- 数据成员一般设为private,成员函数设为public来当做接口,提供给他人调用
- 辅助函数一般也设为private,不提供给他人使用
- 可以在创建子类时,指定调用父类的哪个构造函数
class person
{
private:int age; int sex;
public:
person() //无参的构造函数
{
}
person(int age,int sex)
{
this->age = age;this->sex = sex;
}
};
class teacher:public person
{
private:int level;
public:
teacher(int level) //子类构造函数告诉编译器,调用父类无参的构造函数
{
this->level = level;
}
teacher(int level,int age,int sex):person(age,sex) //子类的构造函数告诉编译器,使用有参的构造函数
{
this->level = level;
}
teacher(int level,int age,int sex) //错误写法
{
person(age,sex) //构造函数person将不会执行,编译器仍然执行无参的构造函数
//不能显式调用
this->level = level;
}
};
int main()
{
//声明子类时,会自动调用父类的构造函数,默认调用无参构造函数;没有构造函数则不调用
teacher t(1);
//调用父类有参的构造函数,同时给3个成员赋值
teacher t(1,2,3)
return 0;
}
2)多态
- 可以用父类对象的指针,访问子类对象的成员,但是只能访问被继承的成员
class A
{
int x; int y;
};
class B:public A
{
int z;
};
A* p = &B; //指针p可以访问子类成员x、y,不能访问z
- 函数重写:在父类和子类中有同名、同参数的函数
- 多态可以让父类指针有多种形态(根据不同的对象来调用不同的函数),C++通过虚函数实现多态性
class person
{
private:int age; int sex;
public:
virtual void print() //虚函数
{
printf("%d %d",age,sex);
}
virtual void print() = 0; //纯虚函数,只是为了提供统一的接口
//父类有纯虚函数,子类一定要进行重写
//含有纯虚函数的类称为抽象类,不能创建对象
//虚函数可以直接使用,也可以被子类重载以后,以多态形式调用
};
class teacher:public person
{
private:int level;
public:
void print() //在子类中重写print
{
person::print();
printf("%d",level);
}
};
void MyPrint(person& p) //子类、父类共用该函数
{
p.print(); //多态:参数是父类的指针,但是仍然可以调用子类的函数
}
int main()
{ //构造函数的代码参考上一节,这里未写明
//父类指针的多态,根据对象的类型不同,调用不同的函数
person a(1,2); //父类对象,调用父类的print
MyPrint(a);
teacher b(1,2,3); //子类对象,调用子类的print
MyPrint(b);
return 0;
}
(9)虚表
- 有虚函数就存在虚表,并且把虚表的地址存储在对象第一个内存单元处(4字节),第二个内存单元才开始存储数据成员
- 父类有n个虚函数,父类的虚表存放的父类虚函数的地址;子类有n个虚函数,子类的虚表存放的子类虚函数的地址,不会存放父类虚函数的地址
- 虚表存储的内容
子类未重写虚函数时
子类重写虚函数时
class A
{
public:
int x;
virtual void test()
{
printf("A");
}
};
class B:public A
{
public:
void test()
{
printf("B");
}
};
void Fun(A* p)
{
p->test(); //Fun函数体现了多态性
mov eax,dword ptr [ebp+8] //把虚表的内存地址的地址存入eax
mov edx,dword ptr [eax] //把虚表的内存地址存入edx
mov esi,esp
mov ecx,dword ptr [ebp+8]
call dword ptr [edx] 间接调用 //虚表中存储的是函数的地址,若调用第2个虚函数则是 [edx+4]
//若未使用虚函数,反汇编代码是
call A::test 直接调用(未添加关键字virtual)
}
printf("%d",sizeof(A)); //输出结果:有虚函数8字节,无虚函数4字节
//无论虚函数的个数,类的大小都只增加4个字节
//这4个字节就是虚表地址,所在的内存地址
printf("%d",sizeof(B)); //输出结果:8字节
(10)运算符重载
//用运算符实现一个函数的功能
class number
{
private:int x; int y;
public:
number(int x; int y)
{
this->x = x; this->y = y;
}
bool max (number& n) //比较两个类的大小
{
return this->x > n.x && this->y > n.y;
}
bool operator> (number& n) //运算符重载
{
return this->x > n.x && this->y > n.y;
}
};
int main()
{
number n1(1,1); number n2(2,2);
bool r = n1.max(n2);
bool r = n1 > n2; //此时>相当于max函数
}
(11)模板
- 在函数中使用模板
//冒泡排序
template <class M> //模板用来替换其他类型
void sort(M* arr, int length) //在调用时,M会被替换成相应的类型
{
int i; int k;
for(i=0;i<length-1;i++)
{
for(k=0;k<length-1-i;k++)
{
if(arr[k]>arr[k+1])
{
M temp = arr[k]; arr[k] = arr[k+1]; arr[k+1] = temp;
}
}
}
}
int main()
{
int arr1[] = {1,5,8,9,7,4}; //M被替换成int
sort(arr1,6); //call 00401090
//编译器重写了函数代码,因此调用地址不同
char arr2[] = {1,5,8,9,7,4};
sort(arr2,6); //call 0040D5F0
return 0;
}
- 在结构体 / 类中使用模板
template <class T, class M>
struct base //用T替换int,用M替换char
{
T x; T y;
M a; M b;
T max()
{
if(x>y) return x;
else{ return y;}
}
M min()
{
if(a<b) return a;
else{ return b;}
}
};
int main()
{
base <int,char> t; //创建一个对象
t.x=1;t.y=2;t.a=3;t.b=4;
int r = t.max();
return 0;
}
(12)纯虚函数
1)概念
- 虚函数:将成员函数声明为virtual
- 纯虚函数:函数没有函数体(后跟 =0)
2)抽象类
- 含有纯虚函数的类,称为抽象类(Abstract Class)
- 抽象类可以包含普通函数,但不能实例化
class bank
{
public:
vitual void max() = 0; //纯虚函数
}
//不能实例化,即不能创建一个对象
bank b;
bank* b = new bank(); //编译器报错
(13)对象拷贝
1)拷贝构造函数
- 拷贝构造函数:创建两个相同的对象,obj 和 objNew 两个对象相同
- 拷贝构造函数在拷贝对象时,同样会拷贝父类的成员
class objecct
{
private: int x; int y;
public:
object() { }
object(int x,int y)
{
this->x = x; this->y = y;
}
//编译器会默认添加构造函数,不需要深拷贝则不需要重写
object(const object& obj) //重写拷贝构造函数,拷贝构造函数参数是固定的
{
}
};
int main()
{
object obj(1,2);
object objNew(obj); //拷贝构造函数
object* p = new obj1(obj);
return 0;
}
2)拷贝构造函数存在的问题
- 若类中含有指针成员,拷贝构造函数在拷贝对象时,只复制指针的值,不复制指针指向的内容(浅拷贝)
- 两个对象中的指针成员指向了同一块内存,当第一个对象内存释放,第二个对象的指针成员将指向空的内存
- 解决方法:重写拷贝构造函数
3)重载赋值运算符
- 赋值运算符和拷贝构造函数存在同样的问题,都是(浅拷贝)
- 如果有父类,要显式调用父类的重载运算符
class base
{
private: int x; int y;
public:
base() { }
base(int x,int y)
{
this->x = x; this->y = y;
}
base& operator=(const base& ref) //在父类中重载赋值运算符
{ ... }
};
class sub:public base
{
private: int z;
public:
sub() { }
sub(int x,int y,int z):base(x,y)
{
this->z = z;
}
sub& operator=(const sub& ref) //要在子类中重载赋值运算符,因为父类的赋值运算符无法覆盖子类的成员
{
//子类能全盘继承父类的成员,除了构造函数和析构函数,所以不能在函数体显式调用父类的拷贝构造函数
base::operator=(ref); //先执行父类的赋值运算符
...
return *this;
}
};
int main()
{
base b1(1,2); base b2(3,4);
b1 = b2;
sub c1(1,2,3); sub c2(3,4,5);
c1 = c2; //使用赋值运算符,实现对象的拷贝(浅拷贝)
return 0;
}
(14)友元
友元函数:在类中添加 friend + 函数原型,可以访问类中的任何成员
友元类:在类中添加 friend + class + 类名,可以访问类中的任何成员
破坏了C++的封装特性
(15)内部类
把一个类定义在另一个类内部,两个类没有任何关系
class outer
{
private: int x;
public:
outer(); //构造函数
class inner
{
private: int y;
public:
inner(); //内部类的构造函数
void Fn(); //函数的定义
};
};
printf("%d",sizeof(outer)); //输出结果为4
outer::inner c; //创建一个内部类的对象,必须处于public
//若内部类处于private,则无法创建内部类对象
//内部类函数的定义与实现
outer::outer() { ... }
outer::inner::inner() { ... }
void outer::inner::Fn() { ... } //函数的实现
(16)命名空间
命名冲突问题
namespace n1{
int x;
void Fn() {...}
class test
{...};
}
namespace n2{
int x;
void Fn() {...}
class test
{
public:
void Fn() {...}
};
}
void Fn() {...} //未指定命名空间,默认为全局命名空间
use namespace n2; //使用命名空间n2的成员可以不用添加前缀
int main()
{
printf("%d",n1::x);
printf("%d",n2::x);
n1::test A; //创建对象
::Fn(); //调用全局命名空间的函数
n2::Fn(); //调用命名空间n2的函数
Fn(); //直接调用,编译器将无法识别
rerurn 0;
}
(17)static关键字
1)面向过程设计中的static
void Fun(int flag)
{
static char buffer[10]; //buffer是只能Fun函数使用的全局变量,不再是局部变量
if(flag)
{ strpy(buffer,"123456"); }
else
{ printf("%s",buffer); }
}
int main()
{
Fun(1); //字符串123456放入buffer数组
Fun(0);
Fun(0);
}
static相当于一个私有的全局变量
extern用来声明函数可以省略,声明变量不能省略
2)面向对象设计中的static数据成员
- 静态数据成员存储在全局数据区,且必须初始化: 数据类型 + 类名 + 静态数据成员名 = 值
- 类的静态数据成员有2种访问形式:
- 类对象名 + 静态数据成员名
- 类类型名 + 静态数据成员名
- 静态数据成员可以避免名称冲突,实现信息隐藏
class base
{
private:
int x; int y;
static int z; //z不在类的内存空间中,z只给base的成员使用,z是全局变量
};
int base::z = 0; //静态成员初始化
int main()
{
base c;
base* p = new base();
printf("%d",sizeof(base)); //输出结果为8
return 0;
}
3)面向对象设计中的static成员函数
- 只有类中的函数定义才能指定关键字static
- 静态成员之间可以互相访问,包括函数与数据之间的访问
- 普通成员函数可以访问静态成员函数和静态数据成员
- 静态成员函数不能访问非静态成员函数和非静态数据成员
- 调用类的静态成员函数的2种方式
- 类 名 + 静态成员函数名 + (参数表)
- 对象名 + 静态成员函数名 + (参数表)
- 指 针 + 静态成员函数名 + (参数表)
class base
{
private:
int x; int y;
static int z; //静态数据成员
public:
base(int x,int y);
static int Getz(); //静态成员函数,只属于base的全局函数
};
int base::z = 0; //静态数据成员初始化
base::base(int x, int y) //实现构造函数
{
this->x = x; this->y = y;
}
int base::Getz() //实现静态成员函数,Getz函数不能访问成员x,y
{ return z; }
int main()
{
base::Getz(); //可直接通过类名访问静态成员函数
base c;
c.Getz(); //通过对象名访问
base* p = new base(); //通过指针
p->Getz();
return 0;
}
4)static的应用:设计模式 —— 单子模式
实现定义的类中只有一个对象被创建,实现方式:
- 禁止对象随便创建
- 保证只存在一份对象
class A
{
private:
A() {} //私有构造函数,无法直接创建对象
static A* PInstance; //静态成员,指向对象的指针
public:
static A* GetInstance() //通过类名调用GetInstance函数,并通过该函数创建对象
{
if(PInstance == NULL)
PInstance = new A();
return PInstance;
}
};
A* A::PInstance = NULL; //静态成员初始化
int main()
{
A* p1 = A::GetInstance();
A* p2 = A::GetInstance(); //将创建同一个对象
return 0;
}
(19集)