c++面向对象的三大特征为:封装,继承,多态。
对象尤其对应的属性和行为,例如:人(对象)身高,体重(属性)吃喝玩乐(行为)。
具有相同性质的对象,可以抽象为类。比如人类。
封装:
1.将属性和行为作为一个整体,表现生活中的事物。
2.将属性和行为加以权限控制。
关于类的一些专用术语:
属性:成员属性,成员变量。
行为:成员函数,成员方法。
类和属性一起被称之为成员。
封装的三种权限:
三种权限用于不同的作用域。
1.public类型,是类内和类外都可以访问的权限。在struct中的默认权限为public。
2.protected类型,在类内可以访问,类外不可以。
3.private类型,在类内可以可以访问,类外不可以。在class中的默认权限为private。
protected类型于private类型的区别在于继承,protected类型的数据可以继承并且更改,private类型的数据不可以继承,只能进行查看不可以进行更改。
将一些重要的成员变量设置为private类型可以保证不被随意修改,相当于是自己控制权限。
构造函数:
用于对象的初始化和清理。
当成员被使用的时候就会被调用的函数,可以自己写,当不写的时候会由编译器默认写一个空的构造函数,当自己写后编译器便不再提供。
要注意:
1.构造函数没有返回值,也不用写void。
2.函数名与类名相同。
3.可以重载。
4.只会被调用一次。
5.调用默认构造函数时不要加括号,会被编译器认为是函数声明。
语法为:函数名(参数){} 分号要不要都可以。
与其他函数一样,构造函数也可以被重载,可以根据传入不同的参数而进入不同的构造函数。
参数可以写也可以不写,构造函数按照参数可以分为有参构造函数和无参构造函数。
按类型分类可以分为普通构造函数和拷贝构造函数。
拷贝构造函数的语法为:
类名(const 类型 & 变量)
作用是将一个变量的所有属性拷贝到另一个变量的所有属性中。
要注意的是拷贝构造函数可以不自己写,编译器会提供,但是编译器提供的拷贝是浅拷贝,如果涉及堆内的地址释放问题,则会报错。
构造函数的常用调用方法是括号法。
例:
#include<iostream>
using namespace std;
class person
{
public:
person(int a)
{
age = a;
}
int age;
};
int main()
{
person a = person (10);
person b(20);
cout << a.age << endl;
cout << b.age << endl;
}
在这段代码中,person10是一个匿名对象,它的特点是匿名对象执行结束后,会被立刻回收。
拷贝构造函数的调用时机:
1.使用一个已经创建完的对象来初始化一个新的对象。
2.值传方式给函数参数传值。
3.以值方式返回局部对象。
2的实例如下:
#include<iostream>
using namespace std;
class person
{
public:
int age;
};
void dowork(person a)
{
cout << a.age << endl;
cout << "a的地址" << &a << endl;
}
int main()
{
person b;
b.age = 20;
dowork(b);
cout << &b << endl;
}
我们通过地址可以观察到a与b的地址并不一致,因为是值传递发生了拷贝。
3的实例如下:
#include<iostream>
using namespace std;
class person
{
public:
int age;
};
person& dowork(person a)
{
return a;
}
int main()
{
person b;
const person *c;
b.age = 20;
c=&dowork(b);
cout << &b << endl;
cout << c << endl;
cout << c->age << endl;
}
我们通过代码也看到了地址不同,但要注意,这种写法十分危险。因为c变成了野指针,其age也变成了随机值。
深拷贝与浅拷贝:
定义:
浅拷贝:简单的赋值拷贝,在对申请的堆区地址进行释放时,会对其释放两次导致系统崩溃。
深拷贝:在堆区重新申请空间,再进行拷贝操作。两次释放对应不同的堆区地址。
深拷贝例子如下:
#include<iostream>
using namespace std;
class person
{
public:
person(int a,int b)
{
age = a;
high = new int(b);
}
person( const person& other)
{
age = other.age;
high = new int(*other.high);
}
int age;
int *high;
~person()
{
delete high;
high = NULL;
}
};
int main()
{
person a(18, 175);
person b(a);
cout << a.age << endl;
cout << *a.high << endl;
cout << b.age << endl;
cout << *b.high << endl;
return 0;
}
还可以用重载运算符的方式实现深拷贝。
例子如下:
#include<iostream>
using namespace std;
class person
{
public:
person(int a)
{
age = new int(a);
}
int *age;
~person()
{
if (age != NULL)
{
delete age;
age = NULL;
}
}
person& operator=(person& a)
{
age = new int(*a.age);
return *this;
}
};
int main()
{
person a(10);
person b(18);
person c(20);
a = b = c;
cout << *a.age << endl;
cout << *b.age << endl;
}
c++对象模型:
1.成员变量和成员函数分开存储。
动态成员变量存储在栈区,如果是通过new得到的成员变量则存储在堆区。
如果是static类型的成员变量则存储在全局区。
成员函数存储在全局区,当调用成员函数时,参数会存储在栈区,函数调用完毕后释放。在成员函数被调用时,会传递一个指向调用它的this
指针为隐含参数。这个this
指针使得成员函数能够访问和修改对象的成员变量。这个this
指针本身也是在栈上分配的,作为函数调用的一部分。
c++编译器会给每个空对象也分配一个字节空间,相当于一个标记。
在C++中,关于空类的大小实际上取决于具体的编译器实现和ABI(应用程序二进制接口)。分配一个字节原因是:
- 唯一性:在C++中,对象的地址是唯一的。即使是一个空类,它的对象也必须有唯一的地址。如果没有为其分配任何空间,那么两个空类的对象可能具有相同的地址,这违反了C++的规则。
- 内存对齐:编译器经常为对象分配内存以使其满足特定的对齐要求。对齐可以提高访问速度并满足某些硬件的要求。如果一个空类不占用任何空间,那么编译器将难以确保对象的正确对齐。
- 继承:如果空类被用作基类,并且派生类添加了成员变量或成员函数,那么基类对象(作为派生类对象的一部分)也需要有空间来确保在内存中的正确布局。
- 虚拟继承:在涉及虚拟继承的情况下,编译器需要为类分配额外的空间来存储虚拟基类指针。如果空类不占用任何空间,那么这些指针将无处可放。
- 与C的兼容性:C++是从C语言发展而来的,C语言中的结构体即使没有任何成员,也会占用一定的空间(通常是一个字节,但这也取决于编译器和平台)。为了使C++与C在ABI上更加兼容,一些编译器可能会选择为空类分配至少一个字节的空间。
请注意,尽管很多编译器为空类分配一个字节的空间,但这并不意味着你可以在内存中直接存储一个字节的值来表示空类的对象。这个空间主要是为了确保对象的唯一性和满足内存对齐等要求,而不是用于存储实际的数据。
this指针:
指向被调用的成员函数所属的对象,本质是一个指针常量,指针的指向是不可以修改的。
而且隐含一个非静态成员函数的一种指针,不需要定义,直接使用即可。
关于其指向可通过下边的代码进行理解:
#include<iostream>
using namespace std;
class person
{
public:
person(int b)
{
a = b;
};
public:
person(const person& b)
{
cout << this << endl;
cout << &b << endl;
}
int a;
};
int main()
{
person c(18);
person d(c);
cout << &c<< endl;
cout << &d << endl;
}
用途:
1.有时候我我们取名很随意,可能会导致形参名和成员变量名字一致,这时候不加this指针的话,会有赋值不准确的风险。
2.类的非静态成员函数中返回对象本身,可以用return *this。
空指针访问成员函数:
在vs2022的测试中,NULL指针只能访问类的成员函数,并且还是不能对成员变量进行修改的成员函数,成员变量可以存在但不可以赋值和修改。如以下所示:
#include<iostream>
using namespace std;
class person
{
public:
int age=0;
void speak()
{
cout << "what can i say??" << endl;
}
}
;
int main()
{
person* p = NULL;
p->speak();
}
这是因为有权限冲突,null指针在c++11中为nullptr是一种空指针常量。而当有对成员变量进行修改时,其本质为:this->变量进行修改,this指针是指针常量。
const修饰成员函数 :
1.不可以修改成员属性。
2.如果成员属性前加了“mutable”后可以修改。
#include<iostream>
using namespace std;
class person
{
public:
mutable int age;
void set_age(int a)const
{
age = a;
}
};
int main()
{
person a;
a.set_age(10);
cout << a.age << endl;
}
加了const的成员函数我们称之为常函数。
其本质上为对this指针再加了一层const;
常对象:声明对象前加一层const,且常对象只能调用常函数。
友元:
在程序中,有些私有属性也想让类外特殊的一些函数或者一些类进行访问。
实现方式有三种:1.全局函数做友元。2.类做友元。3成员函数做友元。
实例1如下:
//实现全局函数做友元
#include<iostream>
using namespace std;
class person
{
public:
friend void people(int a);
int age;
void set_age(int a)
{
age = a;
cout << "年龄为:" << age << endl;
}
private:
int high;
void set_high(int b)
{
high = b;
cout << "身高为:" << high << endl;
}
};
void people(int a)
{
person d;
d.set_high(180);
d.set_age(a);
}
int main()
{
person a;
people(30);
}
实例2如下:
//类做友元
#include<iostream>
using namespace std;
class people;
class person
{
public:
friend people;
int age;
void set_age(int a)
{
age = a;
cout << "年龄为:" << age << endl;
}
private:
int high;
void set_high(int b)
{
high = b;
cout << "高度为:" << high << endl;
}
};
class people
{
public:
void speak()
{
person a;
a.set_age(18);
a.set_high(180);
}
};
int main()
{
people a;
a.speak();
}
实例2中要注意类的声明,哪个类被声明为友元,那么哪个类就需要提前声明。为了避免出错,在没有太多要求且都是新手的情况下,建议有一个类就去声明一个类,那么用他们做友元就很方便。
实例3如下:
#include<iostream>
using namespace std;
class people;
class people
{
public:
void speak(int);
};
class person
{
public:
friend void people::speak(int);
int age;
void set_age(int a)
{
age = a;
cout << "年龄为:" << age << endl;
}
void set_high(int a);
private:
int high;
};
void person:: set_high(int a)
{
high = a;
cout << "身高为:" << high << endl;
}
void people::speak(int)
{
{
person a;
a.set_age(18);
a.set_high(180);
}
}
int main()
{
people a;
a.speak(0);
}
个人认为成员函数做友元的操作是最麻烦的。它的顺序十分讲究,而且效果也很一般。函数方法的实验也必须放在类外且进行说明。
运算符重载:
其中可以对许多操作符进行追加运算,来对自己设计的类型进行运算。关键字为:operator后边追加需要重载的运算符。
例:计算两个成员是否相等,对等号进行重载运算。
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person(string a,int b)
{
name = a;
age = b;
}
string name;
int age;
bool operator==(person& a)
{
if (age == a.age && name == a.name)
return true;
else return false;
cout << this->age << endl;
cout << a.age << endl;
return true;
}
};
int main()
{
person a("zhangsan",18);
person b("zhangsan",20);
if (a == b)
{
cout << "相等" << endl;
}
else
cout << "不相等" << endl;
}
重载方式常用的分为两种:
以operator+为例:
1.成员函数重载:本质是person a=b.operator+(c);
2.全局函数重载:本质是person a=operator(b,c);
运算符重载也可以根据自己的需要设定需要的类型进行运算。
但要注意:
1.对于内置的(int ,float等 )数据类型的表达式运算符不可以改变。不能改变int+int的结果。
2.少使用运算符重载,在实际开发中每个人的想法不一样,会导致多余的错误。
函数调用运算符重载:
1.本质上也就是对“()”运算符进行重载。
2.重载后的使用方式十分像函数调用,因此称为仿函数。
3.仿函数没有固定的写法十分灵活。
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
void operator()(string a)
{
cout << a << endl;
}
};
class test {
public:
int operator()(int a,int b)
{
return a + b;
}
};
int main()
{
int c;
test a;
person b;
c=a.operator()(10, 20);
b.operator()("hello");
cout << c << endl;
cout << a.operator()(10, 20) << endl;
}
简单的实现。
继承:
有点类似生物中的界门纲目科属种。种继承了属,属继承了科……下级别的成员除了拥有上一级的共性,还拥有自己的特性。
继承是多态的基础,很关键。
语法为:
class (子类名):(继承权限) (父类名)
子类也称为派生类,继承权限也称为继承方式,父类也称为基类。
继承方式有三种:
1.公共继承:父类的所有可访问成员在子类中也可以访问。保护权限和私有权限不会被改变。
2.保护继承:父类中的所有成员至少为保护权限。
3.私有权限:父类中的所有成员至少为私有权限。
继承中的对象模型:
在vs2022中如果想观察子类的父类可以用此方法:
1.在开始中找到:
2.复制你要查找的父类所在的文件夹地址。
3.打开第一个图片中的程序
输入cd 进入此文件夹。
4.输入 cl /d1 reportSingleClassLayout后边跟着你要观察的子类名称 再输入cpp的名称即可。
中间加号base class dady是son0的父类。
继承中的构造和析构顺序:
口诀为:白发人生黑发人(先是父类的构造,再是子类的构造),白发人送黑发人(先是子类的析构,再是父类的析构)。
有时候会遇到子类与父类的同名的成员,想要访问分为两种情况:
1.访问子类的同名成员:直接访问即可。
2.访问父类的同名成员:加上作用域即可。
实验代码如下:
#include<iostream>
using namespace std;
class person
{
public:
person()
{
age = 28;
}
int age;
};
class son : public person
{
public:
son()
{
age = 8;
}
int age;
};
int main()
{
son c;
cout << c.age << endl;
cout << c.person::age << endl;
}
要注意的是:如果子类中出现和父类同名的成员函数,那么子类的同名成员会隐蔽掉父类中的所有同名成员函数。例子如下:
#include<iostream>
using namespace std;
class person
{
public:
person()
{
age = 28;
secret0 = 0;
}
int age;
int secret0;
};
class son : public person
{
public:
son()
{
age = 8;
}
int age;
int secret0;
int secret1;
};
int main()
{
son c;
cout << c.age << endl;
cout << c.secret0 << endl;
cout << c.person::age << endl;
}
输出可以看出c.secret0的值并不是0,而是一个随机值,且不会报错。
不管其是静态还是动态访问的方式都是一样的。
多类继承语法:
一类继承多类:class 子类:继承方式 父类,继承方式 父类2,……
要注意的是,多继承可能会引起父类中同名成员的出现,需要加作用域进行区分,且c++实际开发不建议使用多继承。
菱形继承(钻石继承):
会有如下问题:
1.父类1,父类2同时继承父类的父类,当子类使用数据会产生二义性。
2.子类继承两份相同的父类的父类的数据,造成冗余。
可以利用虚继承来解决此问题。
就是在继承的时候加上关键字"virtual".如下所示:
#include<iostream>
#include<string>
//菱形继承
using namespace std;
class person
{
public:
int age;
};
class people :virtual public person
{
public:
int add;
};
class people1 : virtual public person
{
public:
int sub;
};
class son :public virtual people, public virtual people1
{
public:
};
int main()
{
int a = sizeof(son);
int c, d, e;
c = sizeof(people);
d = sizeof(people1);
e = sizeof(person);
cout << a << endl;
cout << c << endl;
cout << d << endl;
cout << e << endl;
}
这样可以解决子类使用成员变量的二义性问题,因为加了virtual之后原本父类1.父类2age成员那块变成了虚指针,指向了虚函数表。这段代码的问题在于在不同的编译器下son的字节大小会发生改变,在clong下大小为40,在vs2022x86的环境下为24,x64的环境为48.
可以用前边提到的ClassLayout来查看,图为以下所示:
在vs2022x64的环境中发生了padding补了8个字节。
所有a的字节大小为48。
多态:
分为静态多态和动态多态。
静态多态:运算符重载,函数重载等。(编译阶段确定地址(地址先绑定))
动态多态:虚函数和派生类等。(运行阶段确定地址(地址后绑定))
应用场景如下:
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak() {
cout << "Animal的输出" << endl;
}
};
class Cat :public Animal {
public:
void speak()
{
cout << "Cat的输出" << endl;
}
};
void dospeak(Animal& animal)
{
animal.speak();
}
int main()
{
Cat cat;
dospeak(cat);
return 0;
}
加了virtual之后会输出cat的讲话,删掉virtual则会变成animal的讲话。
多态的满足条件:
1.要有继承关系。
2.子类要重写父类的虚函数。
重写的概念就是全部一致,除了实现。
当子类继承了父类后,也包括父类中虚函数指针指向的虚函数表中的虚函数地址。
之后子类进行重写虚函数,把继承到的那份父类数据中的虚函数地址改成了子类的虚函数地址。
多态的使用条件:父类的指针或者引用指向子类对象。
多态的好处:
1.组织结构清晰。
2.可读性强。
3.对于后期的维护和前期的发展都很方便,因为在开发中增加新功能不用多态的话要修改源码,
在真实开发环境中,对拓展进行开放,对修改进行关闭。
纯虚函数和抽象类:
纯虚函数语法:virtual 返回值类型 函数名(参数)=0
当类中有纯虚函数时,这个类也称为抽象类。
抽象类特点:
1.无法实例化对象
2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
实例如下:
//用多态计算器的计算。
#include<iostream>
#include<string>
using namespace std;
//先创建一个基类
class base
{
public:
virtual int re() = 0;
int m_a, m_b;
};
class add :public base {
public:
virtual int re() {
cout << m_a << "+" << m_b << "=" << m_a + m_b << endl;
return m_a + m_b;
}
};
int main()
{
base* Base = new add;
Base->m_a = 10;
Base->m_b = 20;
Base->re();
}
虚析构和纯虚析构:
多态使用是,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:
将父类中的析构函数改为虚析构函数或者纯虚析构函数。
实例如下:
//用多态计算器的计算。
#include<iostream>
using namespace std;
//先创建一个基类
class base
{
public:
virtual void re() = 0;
int m_a, m_b;
virtual ~base()
{
cout << "base的构造函数" << endl;
}
};
class add :public base {
public:
virtual void re() {
cout << m_a << "+" << m_b << "=" << m_a + m_b << endl;
}
~add()
{
cout << "add的析构函数" << endl;
}
};
int main()
{
base* Base = new add;
Base->m_a = 10;
Base->m_b = 20;
Base->re();
delete Base;
}
共性:
1.可以解决父类指针释放子类对象。
2.都需要具体的函数实现。
区别:
纯虚函数属于抽象类,无法实例化对象。