思维导图
面向对象
1.面向对象思想
概念:面向对象编程(OOP)是一种以对象为基础的编程范式,强调将数据和操作数据的方法封装在一起。这就是上篇文章讲过的。面向过程是以“怎么解决问题”为核心,而面向对象思想在于“谁来解决问题”为核心。
特点:
1. 将操作的事物看成对象
2. 不需要自己亲自去做事,而是直接调用对象的行为完成需要的操作
3. 简化复杂的分步操作,提高编程效率
2.类和对象
类:类是一个概念,用于描述具体对象的特点,包括属性(数据)和行为(函数)。自定义类的命名必须使用帕斯卡命名法:所有单词的首字母大写。
属性:也就是成员变量,具体的数据。
行为:也就是成员函数,具体的函数,可以实现一定的方法。
对象:对象是依照类创建的实体,可以有多个,但是多个对象之间的数据是相互隔离的
一.类和对象的创建和使用
想象现在你就是上帝,你创造一只猫,它有年龄身高名字等基础属性,这些就是这个猫的“数据”,它还会奔跑,喵喵叫等,这些就是这个“数据”的动作,也就是“方法”,在C++中就可以把数据和方法封装在一起,这就有了面向对象的第一个属性----封装。这样一来,这个对象就不只是一个数据,还包含它的方法,就可以让这个对象完成一些特定行为,而不是我创造猫,我让猫叫,让猫跑,而是我创造了猫,让猫自己叫,自己跑。
1.创建类
#include <iostream>using namespace std;class Cat{
public: //公有成员,可以在外部访问string name; //属性string color;int age;double weight;void run(){ //行为:跑cout << "run for you" << endl;}void meow(int a){ //叫if(a == 8)cout << "miao miao" << endl;}void name1(){ //显示名字name = "mike";cout << name << endl;}
};int main()
{return 0;
}
2.创建对象
(1)栈内存对象:
只存活在最近的{}内,{}执行结束后,自动被销毁,适合临时使用。
Cat cat1;
(2)堆内存对象:
使用new关键字创建,使用delete关键字销毁,如果不销毁则持续存在,会导致内存泄漏。堆内存对象使用指针存储首地址,使用->调用成员。
Cat *cat2 = new Cat;
3.使用对象
当使用的pubulic修饰的类,可以直接在外部赋值,使用在对象后面加“.”就可以使用内部函数。
#include <iostream>using namespace std;class Cat{
public: //公有成员,可以在外部访问string name; //属性string color;int age;double weight;void run(){ //行为:跑cout << "run for you" << endl;}void meow(int a){ //叫if(a == 8)cout << "miao miao" << endl;}void name1(){ //显示名字name = "mike";cout << name << endl;}
};int main()
{//栈内存对象Cat cat1;cat1.name = "Mike";cat1.color = "yellow";cat1.age = 8;cat1.weight = 35.7;/*if(1){ //错误,创建的栈内存对象只能存活在最近的两个{}内,if一结束就不存在Cat cat1;}*/cout << cat1.name <<endl;cout << cat1.color <<endl;cout << cat1.age <<endl;cout << cat1.weight <<endl;cout << "#################" <<endl;cat1.run();cat1.meow(8);cat1.name1();cout << "#################" <<endl;//堆内存对象Cat *cat2 = new Cat;cat2->name = "Join";cat2->color = "red";cat2->age = 7;cat2->weight = 44.2;cout << cat2->name <<endl;cout << cat2->color <<endl;cout << cat2->age <<endl;cout << cat2->weight <<endl;return 0;
}
二.封装
概念:上面的代码与结构体非常相似,因为一个完全开放的类就是结构体。实际上类需要封装,封装需要先把类中的一些属性和细节隐藏,根据实际的需求对外部开放调用的读写等接口。
外部读取数据的接口称为getter,外部写入数据的结构称为setter。
核心思想:隐藏内部实现细节,通过公共接口控制访问
封装优势:
(1)数据保护(防止非法修改)
(2)实现与接口分离
(3)代码可维护性增强
#include <iostream>using namespace std;class Cat{
private: // 私有成员(封装的核心)string name;string color;int age;double weight;
public: // 公共接口string get_name(){ //得到(返回)名字return name;}void set_name(string a){ //设置名字name = a;}string get_color(){ //得到(返回)颜色return color;}void set_color(string a){ //设置颜色color = a;}//尝试自己写一下其他操作
};int main()
{//栈内存对象Cat cat1;cat1.set_name("Mike"); //设置名字cout << cat1.get_name() << endl;cat1.set_color("yellow"); //设置颜色cout << cat1.get_color() << endl;cout << "#################" <<endl;//堆内存对象Cat *cat2 = new Cat;cat2->set_name("Lisa"); //设置名字cout << cat2->get_name() << endl;cat2->set_color("red"); //设置颜色cout << cat2->get_color() << endl;delete cat2;return 0;
}
三.构造函数
构造函数(constructor)是一种特殊的成员函数:
(1)用于创建对象,创建对象必须调用构造函数
(2)如果一个类中程序员不写构造函数,编译器会自动添加一个无参的构造函数
(3)不写返回值类型,返回一个创建的对象
(4)函数名称必须是类名
构造函数的最主要功能是在创建对象时,完成对象数据的初始化。
可以和函数重载联合使用。
1.初始化构造函数:
#include <iostream>using namespace std;class Cat{
private:string name;string color;int age;double weight;
public:Cat(){ name = "mike";color = "red";age = 4;weight = 33.5;}//函数重载Cat(string a,string b,int c,double d){name = a;color = b;age = c;weight = d;}void printf(){cout << name << endl;cout << color << endl;cout << age << endl;cout << weight << endl;}
};int main()
{//栈内存对象Cat cat1; //不使用参数默认调用无参的构造函数cout << "#################" <<endl;cat1.printf();cout << "#################" <<endl;Cat cat2("Join","blue",8,66.2); //调用有参的构造函数cat2.printf();cout << "#################" <<endl;//堆内存对象Cat *cat3 = new Cat("Lisa","red",1,12.5); //调用有参的构造函数cat3->printf();delete cat3;return 0;
}
2.构造初始化列表:
#include <iostream>using namespace std;class Cat{
private:string name;string color;int age;double weight;
public:Cat(){name = "mike";color = "red";age = 4;weight = 33.5;}//函数重载,构造初始化列表Cat(string a,string b,int c,double d):name(a),color(b),age(c),weight(d){/*name = a;color = b;age = c;weight = d;*/}void printf(){cout << name << endl;cout << color << endl;cout << age << endl;cout << weight << endl;}
};int main()
{//栈内存对象Cat cat1; //不使用参数默认调用无参的构造函数cout << "#################" <<endl;cat1.printf();cout << "#################" <<endl;Cat cat2("Join","blue",8,66.2); //调用有参的构造函数cat2.printf();cout << "#################" <<endl;//堆内存对象Cat *cat3 = new Cat("Lisa","red",1,12.5); //调用有参的构造函数cat3->printf();delete cat3;return 0;
}
3.调用方式:
(1)显式调用
明确指定使用构造函数,下面代码中cat1~3都是显式调用。
(2)隐式调用
没有明确指定使用构造函数,当隐式调用是一个字符串类型(Cat cat6 = "Lisa";),或报错,编译器没办法优化复杂这样复杂的情况。下面代码中cat4和cat5都是隐式调用。
#include <iostream>using namespace std;class Cat{
private:int age;double weight;string name;string color;
public:Cat(){age = 4;weight = 33.5;name = "mike";color = "red";}//函数重载,构造初始化列表Cat(int a,double b = 25.3,string c = "mike", string d = "red"):age(a),weight(b),name(c),color(d){cout << a << endl;}void printf(){cout << "年龄:" << age << endl;cout << "体重:" << weight <<endl;cout << "名字:" << name << endl;cout << "颜色:" << color << endl;}
};int main()
{//栈内存对象Cat cat1; //不使用参数默认调用无参的构造函数cout << "#################" <<endl;cat1.printf();cout << "#################" <<endl;Cat cat2(8); //调用有参的构造函数cat2.printf();cout << "#################" <<endl;//堆内存对象Cat *cat3 = new Cat(1); //调用有参的构造函数cat3->printf();delete cat3;cout << "#################" <<endl;Cat cat4 = 8; //编译器优化,可以把8作为参数传入(隐式调用)cat4.printf();cout << "#################" <<endl;Cat cat5 = {4,44.3,"Lisa","red"}; //隐式调用cat5.printf();//Cat cat6 = "Lisa"; //当第一个参数是字符串类型,就不会优化,对于编译器而言太复杂return 0;
}
补:可以使用explicit屏蔽隐式调用
隐式调用一般用不到,也不好用,使用不安全,容易出现错误
#include <iostream>using namespace std;class Cat{
private:int age;double weight;string name;string color;
public:explicit Cat(){age = 4;weight = 33.5;name = "mike";color = "red";}//函数重载,构造初始化列表explicit Cat(int a,double b = 25.3,string c = "mike", string d = "red"):age(a),weight(b),name(c),color(d){cout << a << endl;}void printf(){cout << "年龄:" << age << endl;cout << "体重:" << weight <<endl;cout << "名字:" << name << endl;cout << "颜色:" << color << endl;}
};int main()
{//栈内存对象Cat cat1; //不使用参数默认调用无参的构造函数cout << "#################" <<endl;cat1.printf();cout << "#################" <<endl;Cat cat2(6); //调用有参的构造函数cat2.printf();cout << "#################" <<endl;//堆内存对象Cat *cat3 = new Cat(4); //调用有参的构造函数cat3->printf();delete cat3;cout << "#################" <<endl;/* //隐式调用失效Cat cat4 = 8; cat4.printf();cout << "#################" <<endl;Cat cat5 = {4,44.3,"Lisa","red"}; //隐式调用cat5.printf();*/return 0;
}
4.拷贝构造函数
拷贝就是复制,创建一个和原对象一样的新对象,被拷贝的对象和拷贝到对象的存储地址是不一样的,不是引用。
在每个类创建的时候,如果程序员不写,都自己会带一个默认的拷贝构造函数(浅拷贝函数)。
//拷贝构造函数(不写也可以,默认存在)Cat(const Cat &a){age = a.age;name = a.name;}Cat cat1(); //创建第一对象 Cat cat2(cat1); //拷贝一个对象
1.浅拷贝
如果成员变量出现指针类型,默认的拷贝构造函数会直接拷贝指针变量,多个变量保存在同一个地址,这些对象的成员变量都指向同一块内存,这样的成员变量不符合面向对象的要求,会导致成员变量随着这块空间的内容的变化而变化,还记得我们封装的时候的核心思想是什么?隐藏内部实现细节,成员变量为什么要用private修饰?就是为了防止外部改变,但是拷贝存在指针类型的对象就会出现成员变量随着外部变化而变化的情况。
重写一下代码(写简单点):
#include <iostream>
#include <string.h>
using namespace std;class Cat{
private:int age;char *name;
public://默认的拷贝构造函数(不写也可以)//构造初始化列表explicit Cat(int a,char *b):age(a),name(b){cout << a << endl;}void printf(){cout << "年龄:" << age << endl;cout << "名字:" << name << endl;}
};int main()
{char name[5] = "Lisa";Cat cat1(6,name); //调用有参的构造函数cat1.printf();cout << "#################" <<endl;Cat cat2(cat1); //拷贝构造函数cat2.printf();cout << "#################" <<endl;strcpy(name,"Join"); //此时我们改变name内的值cat2.printf(); //因为构造类我们用的是指针,所以里面的值发生了改变return 0;
}
2.深拷贝
出现上述情况,我们需要修改拷贝构造函数和初始化构造函数,在初始化构造函数的时候,开辟空间,再把内容复制到新开的空间里,这样就避免了成员变量指向外部空间。在C++中使用new开辟空间。
开辟堆空间:
char *a = new char[10];
优化代码:
#include <iostream>
#include <string.h>
using namespace std;class Cat{
private:int age;char *name;
public:Cat(const Cat &d){age = d.age;name = new char[5];strcpy(name,d.name);}explicit Cat(){age = 4;name = new char[5]; //开辟空间strcpy(name,"mike"); //复制内容到空间}//函数重载,构造初始化列表explicit Cat(int a,char *b){age = a;name = new char[5]; //开辟空间strcpy(name,b); //复制内容到空间cout << b << endl;}void printf(){cout << "年龄:" << age << endl;cout << "名字:" << name << endl;}
};int main()
{char name[5] = "Lisa";Cat cat1(6,name); //不使用参数默认调用无参的构造函数cat1.printf();cout << "################" <<endl;Cat cat2(cat1);cat2.printf();cout << "################" <<endl;strcpy(name,"Join");cat2.printf();return 0;
}
四.析构函数
学习上面内容后,还有一个问题,我们使用New开辟的堆空间存在一个问题,这个空间是由程序员开辟和释放的,程序结束才会释放。就像上面代码,我们创造一个栈空间对象,但是内部的name是开的堆空间存放,当对象所在的{}结束,对象cat1就会释放,当时name没有释放,会出现内存泄露。
1.什么是内存泄露?
内存泄露指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存浪费,导致程序运行速度减慢或崩溃。这类问题在长时间运行的应用中尤为严重。若果不解决,运行过程总会堆积这样的垃圾空间。
2.析构函数
这个时候就可以使用析构函数来解决;析构函数(destructor)是与构造函数对立的函数,也是一种特殊的成员函数,如果程序员不手写,编译器会自动添加一个空的析构函数;析构函数是用来对对象资源的回收,关闭和释放。下面就是析构函数初始版本(不写的默认):
~Cat(){}
这也没参数啊,怎么做到的?别管,问就是C++特性,感兴趣可以去看源码。现在我们在析构函数中加上delete name;专门去释放存在堆区域的name变量,就解决了内存泄露的问题。
3.优化代码:
#include <iostream>
#include <string.h>
using namespace std;class Cat{
private:int age;char *name;
public:explicit Cat(){age = 4;name = new char[5];strcpy(name,"mike");}//函数重载,构造初始化列表explicit Cat(int a,char *b){age = a;name = new char[5];strcpy(name,b);cout << b << endl;}//拷贝构造函数Cat(const Cat &a){age = a.age;name = a.name;}void printf(){cout << "年龄:" << age << endl;cout << "名字:" << name << endl;}~Cat(){ //析构函数,为防止内存泄露cout << "析构函数" << endl;delete name;}
};int main()
{char name[5] = "Lisa";Cat cat1(6,name); //不使用参数默认调用无参的构造函数cat1.printf();cout << "################" <<endl;Cat *cat2 = new Cat;cat2->printf();delete cat2;cout << "################" <<endl;cout << "结束" << endl;return 0;
}
4.析构函数和构造函数比较:
析构函数 | 构造函数 |
没有参数 | 可以有参数,支持默认值和重载 |
函数名称是 ~类名 | 函数名称是 类名 |
对象销毁时被调用 | 创建对象时调用 |
各种的资源的回收、关闭和释放 | 数据初始化,各种资源的开辟 |
五.作用域限定符 ::
概念:
作用域限定符 ::
(也称为范围解析运算符)在C++中,是一个关键运算符,用于明确指定标识符(变量、函数、类等)所属的作用域。它的主要功能是消除命名冲突,并提供对特定作用域成员的精确访问。
1.名字空间
名字空间(namespace)是C++用于解决重名问题设计的。在上一篇文章中提到过。
#include <iostream>//using namespace std;//std是C++源码的名字空间
int a = 1;
// 自定义名字空间
namespace my {int a = 3;int b = 4;
}
using namespace my;int main()
{int a = 2;std::cout << a << std::endl; // 2// 匿名名字空间std::cout << ::a << std::endl; // 1std::cout << my::a << std::endl; // 3std::string s = "123";std::cout << b << std::endl; // 4return 0;
}
2.类内声明,类外定义
类内的成员(尤指成员函数)可以声明与定义分离,声明要在类内,定义可以写在类外,这个以后会用的比较频繁。
#include <iostream>using namespace std;class Teacher
{
private:string name;
public:// 只声明Teacher(string n);string get_name();void set_name(string n);
};
// 类外定义
Teacher::Teacher(string n)
{name = n;
}
string Teacher::get_name()
{return name;
}
void Teacher::set_name(string n)
{name = n;
}int main()
{Teacher t1("t1");cout << t1.get_name() << endl;t1.set_name("t2");cout << t1.get_name() << endl;return 0;
}
3.与static关键字配合
在 C++ 中,作用域限定符 ::
与 static
关键字 配合使用主要涉及类的静态成员管理,这是实现类级别数据共享的核心机制。(后面再讲)