类与对象属于面向对象的程序设计思想(Object Oriented Programming),简称OOP。
面向对象基础理论
面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物,是一种软件开发的方法
面向对象四大特性
1.抽象
忽略一个主题中与当前目标无关的东西,专注的注意与当前目标有关的方面。就是把现实世界中的某一类东西提取出来,用程序代码表示,抽象出来的一般叫做类或者接口。抽象并不打算了解全部问题,而是选择其中一部分,暂时不考虑部分细节。抽象包括两个方面,一个是数据抽象,一个是过程抽象。
数据抽象:表示世界中一类事物的特征,就是对象的属性。比如鸟有翅膀、羽毛等(类的属性)
过程抽象:表示世界中一类事物的行为,就是对象的行为。比如鸟会飞(类的方法)
2.封装
封装是面向对象的特征之一,是对象和类概念的主要特征。封装就是把过程和数据包围起来,对数据的访问只能通过已定义的界面。如私有变量,用set和get方法获取。
封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低程度。
3.继承
一种联结类的层次模型,并且允许和鼓励类的重用,提供一种明确表达共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特称,新类称为派生类(子类),原始类被称为新类的基类(父类)。派生类的可以从它的父类那里修改或增加新的方法,使之更适合特殊的需要。因此可以说,继承不仅能重用父类代码,同时为实现多态性作了准备。
继承的原则:继承使得一个对象可以获取另一个对象的属性。使用继承可以让已经测试完备的功能得到复用。并且可以一次修改,所有继承的地方都同时生效。
4.多态
多态是指允许不同类的对象对同一消息做出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活/抽象/行为共享/代码共享的优势,很好的解决了应用程序函数同名的问题(函数重载,方法重写,方法的动态链接)。
动态链接:对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将调用子类中的这个方法,这就是动态链接。
访问权限修饰符
定义时的修饰符
此处修饰符多指访问修饰符,在C++中共有三种访问修饰符:
public:公共的。可以被任何类访问。可通过对象访问
private:私有的。只能被同一类内的方法访问,其它任何类(包括子类)都无法访问。也可以被友元函数访问。不可通过对象访问。
protected:受保护的。可以被同一类访问,也可以被子类访问。不可由其他类访问。不可由对象访问。
继承时的修饰符
public:使用public继承,父类的方法属性不会发生改变
private:使用private继承,父类的所有方法在子类中变为private,不允许子类使用。
protected:使用protected继承,父类的protected和public方法在子类变为protected,private方法不变。
初识:类与对象
类和对象概述
类(Class):具有相同特征的事物的抽象描述,是抽象的、概念上的定义。即:具有相同属于与行为的一系列事物的统称。例如:人类,动物类,车类等
对象(Object):实际存在的该类事物的每个个体,是具体的,因而也被称为实例具有具体属性和行为的每一个事物都可称之为对象。(万物皆可对象)
成员变量(field)
语法格式:
[修饰符] class 类名{
[修饰符] 数据类型 成员变量名[=初始化值];//属性
}
成员函数(member function)
[修饰符] class 类名{
[修饰符] 返回值类型 成员函数(参数列表){
函数体;
}//方法
}
接口与抽象类
认识:抽象类与接口
一、抽象类
抽象类是特殊的类,只能被继承,无法实例化;除此以外,抽象类具有类的全部特征;重要的是抽象类可以包含抽象方法,而普通类不能声明或定义抽象的方法。
抽象方法:不包含任何实现,由派生的子类完成内部方法体内容(必须实现覆盖)
注意:抽象类可以派生一个抽象子类。抽象子类可以覆盖基类的抽象方法也可以不覆盖,如果不覆盖,则该派生类的派生类必须覆盖它们。
二、接口
接口是引用类型的,类似于“类”,和抽象类的相似之处:
1.无法实例化
2.包含未实现的方法声明
3.派生类必须实现未实现的方法,抽象类是抽象方法,接口则是所有的成员(不仅是方法,还包括其他成员)
接口的特性:接口除了可以包含方法以外,还可以包含属性、索引器、事件等,而且这些成员都被定义为公有的(public)。除此以外,不能包含任何其它成员,例如常量、域、构造函数、析构函数、静态成员。
Tips:“一个类可以直接继承多个接口,但只能直接继承一个类(包括抽象类)”
区别:抽象类与接口
1.抽象类是对类的抽象,(类是对对象的抽象),可以把抽象类理解为对象为类的类。而接口只是一个行为的规范或规定,微软的自定义接口总是后带able字段,证明其是表述一类类“我能做......”。抽象类更多的是定义在一系列紧密关系的类之间,而接口大多是关系疏松但都实现某一功能的类中。
2.接口不具有继承的任何特点,它仅仅承诺了能够调用的方法。
3.一个类一次能实现若干个接口,只能拓展一个父类,却可以同时拓展多个接口。
4.接口可以用于支持回调,而继承不具备这个特点
5.抽象类不能被密封。
6.抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法默认为非虚的,当然也可以显式声明为虚的。
7.接口与非抽象类类似,抽象类也必须为在该类的基类列表中列出的接口的所有成员提供它自己的实现。但是,允许抽象类将接口方法映射到抽象方法上。
8.抽象类实现了oop中的一个原则,把可变与不可变进行分离。抽象类和接口就是定义为不可变的,而把可变的作为子类去实现。
9.好的接口定义应该具有专一功能性的,而不是多功能的,否则会造成接口污染。
接口污染:如果一个类只是实现了这个接口中的一个功能,而不得不额外实现接口的其它方法,这个现象就是接口污染。
10.如果用抽象类实现接口,则可以把接口中的方法映射到抽象类中作为抽象方法而不必实现,但需要在抽象类的子类中实现接口中的方法
11.尽量避免使用继承来实现组件功能,而是使用黑箱复用,即对象组合。因为继承的层次增多,造成最直接的后果就是当你调用这个类群中某一类,就必须把它们全部加载到栈中,后果可想而知。同时,有心的朋友可以留意到微软在构建一个类时,很多时候用到了对象组合的方法。比如:asp.net中的Page类,有Server Request等属性,但其实它们都是某个类的对象。使用Page类的这个对象来调用另外的类的方法和属性,这个是非常基本的一个涉及原则。(其实在这里,我个人觉得合适的例子就是:优先级队列类priority_queue的底层实现可以是vector类,这个就是用vector类的一些方法来构建priority_queue类的一些功能)
对象的初始化与清理
1.构造函数
主要作用是在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
语法:类名(){}
class A{
A(){
//没有返回值,不写void
//函数名称与类名相同
//构造函数可以有参数,因此可以进行重载
//程序在调用对象时自动调用构造函数,无序手动调用,而且只调用一次
//当构造函数发生重载时,编译器根据参数列表自动匹配构造函数
//不写构造函数时,该类默认构造函数即:A(){}
}
};
预备知识:new运算符的基本使用,方法重载:C++函数重载-CSDN博客
class Pointer {
private:
int* p = nullptr;
int size = 0;
public:
Pointer() { cout << "无参构造" << endl; }
Pointer(int num) {
cout << "有参构造" << endl;
if (num <= 0)return;
size = num;
p = new int[num];
}
Pointer(int* x)
:p(x)
,size(size+1)
{cout << *p << endl ;}
};
int main() {
int x = 3;
Pointer p1;//无参构造
Pointer p2(10);//有参构造-1
Pointer p3(&x);//有参构造-2
return 0;
}
在上述代码中,我们创建了三个Pointer类 类型的变量,创建时,p1无参数传入,采用无参构造;p2传入要开辟的堆区空间数量为10,那么 通过new运算符开辟10个空间,空间大小size更新为10;p3通过初始化列表进行初始化。
初始化列表:以冒号开始,逗号分隔,括号中给值
//写法1.
Pointer(int x):p(x),size(1){
}
//写法2.
Pointer(int x)
:p(x)
,size(1)
{}
注意:
1.只能在构造函数中使用
2.初始化顺序和成员变量定义顺序一致
3.常量和引用要在初始化列表中初始化
初始化顺序与初始化列表顺序无关:
class A {
int a, b, c;
public:
A(int a, int b, int c)
:a(a)
,b(b)
,c(c)
{cout << a << " " << b << " " << c << endl;}
A(int b1,int c1)
:c(c1)
,a(c)
,b(b1)
{cout << a << " " << b << " " << c << endl;}
};
int main() {
A a(1, 2, 3);
A aa(1, 2);
return 0;
}
oi!,结果有点不如人意哦,怎么会出现了-858993460呢?这就是我们所说的初始化顺序与初始化列表顺序无关,初始化顺序与成员变量定义时的顺序有关:首先给a初始化,赋值时是c的值,c没有初始化,采用随机值;然后给b初始化b1:1;然后给c初始化c1:2。最后我们的结果就是 随机 1 2;并不是2 1 2。
构造函数调用方式:
1.括号法:上述都是用括号法写的
Pointer p(3);
2.显式法:像函数的使用一样
Pointer p = Pointer(3);
3.隐式转换法:使用等号=
Pointer p = 3;
//在此次,会调用参数为int类型的构造函数,将3 转化为Pointer类型的对象
注意:Pointer p();这种写法并不是括号调用无参构造,它会被编译器识别为函数声明,返回值为Pointer ,参数列表为空,函数名为p的一个函数。
构造函数调用规则:
默认情况下,C++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数:对属性进行值拷贝
调用的具体规则:如果用户定义有参构造函数,c++不再提供默认无参构造函数,但会提供默认拷贝构造函数。如果用户定义拷贝构造函数,c++不会提供其它构造函数。
构造函数习后练习:
练习一:创建一个矩形类,属性包括长宽,通过构造函数初始化举行对象,打印矩形的面积。通过三种方式分别调用构造函数。
class Rectangle{
int width;
int length;
public:
Rectangle()
:width(0)
,length(0)
{}
Rectangle(int width,int length){
this->width=width;
this->length=length;
}
void Print_S()
{cout<<width*length<<endl;}
};
int main()
{
//无参构造
Rectangle r;
r.Print_S();
//括号法
Rectangle r1(1,2);
r1.Print_S();
//隐式法
Rectangle r2 = {1,2};
r2.Print_S();
//显式法
Rectangle r3 = Rectangle(1,2);
r3.Print_S();
return 0;
}
练习二:创建一个person类,其中属性包括:姓名,性别,年龄,循环输入3个人并且打印出每个人的信息 。
class Person{
string name;
string sex;
int age;
public:
Person(string name,string sex,int age){
this->name=name;
this->sex=sex;
this->age=age;
}
void showInfo(){
cout<<"姓名:"<<name<<endl;
cout<<"性别:"<<sex <<endl;
cout<<"年龄:"<<age <<endl;
}
};
int main()
{
vector<Person> vec={Person("张三","男",18),Person("小丽","女",1),Person("李四","男",0)};
for(auto e:vec){
e.showInfo();
}
Person* per = new Person("张三","男",11);
delete per;
Person* pers=new Person[3]{Person("张三","男",18),Person("小丽","女",1),Person("李四","男",0)};
for(int i=0;i<3;i++){
pers[i].showInfo();
}
delete[] pers;
return 0;
}
2.析构函数
主要作用是在对象销毁前系统自动调用,执行一些清理工作
语法:~类名(){}
class A{
~A(){
//析构函数,没有返回值也不写void
//函数名称和类名相同,在名称前符号 ~
//析构函数不可以有参数,因此不可以发生重载
//程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
}
};
class Pointer{
int* p;
public:
Pointer()
:p(nullptr)
{}
Pointer(int num){
p=new int(num);
}
~Pointer(){
if(p) delete p;
}
};
int main()
{
Pointer p1(3);//栈区
//new 会先调用malloc分配足够的内存,再掉用构造函数给分配的内存赋值
Pointer *p2=new Pointer(3);
delete p2;
//delete 先调用析构函数,再调用free释放内存
//new 和 malloc区别?
return 0;
}
3.this指针
一个对象的this指针并不是对象本身的一部分,不影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员时,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。
this是指向当前对象的指针,哪个对象调用包含this指针的函数,this指向哪个对象。
this一般在构造函数中使用,用于区分成员变量和参数。
class A{
public:
int num;
/*
this,是成员函数的隐含参数,当对象调用成员函数时,会将对象的地址赋值给this指针
this 的作用?
1.使用this区分成员变量和形参
2.在函数内区分不同对象的成员变量
*/
void set_num(int num){
this->num=num;
}
};
4.拷贝构造函数
C++中拷贝构造函数调用时机通常有三种情况:
1.使用一个已经创建完毕的对象来初始化一个新对象(直接调用)
2.值传递的方式给函数参数传值(实参初始化形参)
3.以值的方式返回局部对象
注意:参数或者返回值为引用类型时,可以避免调用拷贝构造
拷贝构造:通过当前的对象复制出一份一摸一样的对象
class A{
private:
int num;
public:
A():num(0){//无参构造
}
A(int a){//有参构造
num=a;
}
A(const A& other){//拷贝构造
num=other.num;
}
}
void test01(){
A a;//无参构造
A b = a;//隐式调用拷贝构造
A c(a);//用已经存在的对象初始化新的对象
A d= A(a);//显式调用拷贝构造
}
void test02(A a){
//类类型做参数
}
A test03(){
A a;
return a;
}
int main(){
//test01();
//A p;test02(p);
A a = test03();//用来接收
return 0;
}
对于拷贝构造, 参数必须为引用,不然就会陷入无限递归,递归就是函数自己调用自己。
在这里我们无法验证递归的现象
VS2022:(直接报错)
DevC++ :(直接优化为const A&)
*深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑。
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作。
#include <bits/stdc++.h>
using namespace std;
#define Long long long
class A{
int* p;
int num;
public:
A() {//无参构造
this->num = 0;
p = nullptr;
}
A(int a) {//有参构造
this->num = a;
p = new int[num];
for (int i = 0; i < num; i++) {
p[i] = 0;
}
}
A(const A& other) {//拷贝构造
/*
* 浅拷贝:简单的赋值操作,保证值相等
* 浅拷贝问题:
* 如果有指针指向堆区内存时,不同对象的指针成员指向同一堆区内存
* 在对象释放时,该堆区内存会被释放两次,当一个对象修改堆区内存时,另一个对象也会随着变化
* p=other.p;
* this->num=other.num;
*/
//深拷贝:申请相同大小的堆区空间,保证两个对象的堆区值相同
num = other.num;
p = new int[num];
for (int i = 0; i < num; i++) {
p[i] = other.p[i];
}
}
};
int main() {
A a(5);//有参构造
A b(a);//深拷贝
//A o;//无参构造
A o(1);
A c(o);//浅拷贝
return 0;
}
深拷贝内存地址:
浅拷贝内存地址:
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的多对象共享同一造成的内存问题。
类对象作为类成员
在本目录中“接口与抽象类”的区别版块,第11条就提到了类对象作为类的成员的情况。这十分常用。
C++类中的成员可以是另一个类的对象,我们称成员为对象成员。
例如:
class A{};
class B{
A member-a;
};
B类中有对象A作为成员,A作为对象成员。
那么当创建B时,A与B的构造和析构的顺序是谁先谁后呢?
class B{
public:
B(){
cout<<"构造B"<<endl;
}
~B(){
cout<<"析构B"<<endk;
}
}
class A{
B b;
public:
A(){cout<<"构造A"<<endl;}
~A(){cout<<"析构A"<<endl;}
}
int main(){
//创建对象a时,先创建成员变量b(B构造),再调用A的构造函数给成员变量赋值
A a;
return 0;
}
静态成员(static)
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:静态成员变量,静态成员函数
1.静态成员变量
1.所有对象共享一份数据
2.编译阶段分配内存,在主函数前进行构造
3.类内声明,类外初始化
4.在发生继承时,静态成员变量不会被继承,父类子类共享一个静态成员
5.可以使用类或对象访问公有的静态成员变量
6.静态成员变量不占对象的内存(不计入sizeof(对象)的结果)
这里C++sizeof的用法支持sizeof直接加需要计算的类型或数据,更符合sizeof的本质--运算符的写法,而不是c语言的函数风格
tips:空类的大小为1。
2.静态成员函数
1.静态成员函数只能访问静态成员变量
2.没有this指针,所以静态成员函数内部不可以使用非静态的成员变量和成员函数
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针。如果用到this指针,需要加以判断保证代码的健壮性。
class Person{
int num;
public :
void work(){
cout<<"work"<<endl;
}
void fun(){
num=2;
}
};
int main(){
Person* p=nullptr;
p->work();//bingo正确,输出work
p->fun();//error,错误。通过指针来调用成员函数时,就是把p的值给成员函数的this,现在p为空,那么在函数体访问this->num时就会出错
return 0;
}
const修饰成员函数
本节知识比较重要,之后会单写一篇博客(知识点补充专栏)进行讲解,后续会把链接加到这块。
友元
友元的目的是让一个函数或者类,访问另一个类中私有的成员。
友元的关键字为friend 是一个修饰符
友元分为友元类和友元函数
1.全局函数做友元
2.类做友元
好处:可以通过友元在类外访问类内的私有和受保护类型的成员
坏处:破坏了类的封装性
class A {
int num_a;
public:
friend void fun();
friend class B;
A() { cout << "构造-A" << endl; }
~A() { cout << "析构-A" << endl; }
};
A a;
void fun() {
a.num_a = 2;
cout << a.num_a << endl;
}
class B {
public:
void fun() {
a.num_a = 2;
}
B() { cout << "构造-B" << endl; }
~B() { cout << "析构-B" << endl; }
};
int main() {
fun();
return 0;
}
不辞辛苦,码了近万字,初步介绍认识C++中的面向对象编程知识,希望点赞+收藏走一波,之后可能还会在本节进行后续的修改补充等。感谢大家!