面向对象概念
类与对象
构造函数和析构函数
内存的动态分配★★★
对象数组和对象指针
函数参数的传递机制
友元
1. 面向对象的概念
面向对象的三大特征:封装、继承、多态
先学习封装,看完类和对象之后再看继承和多态:
- 概念:C++中封装是通过类来实现的,一个类就是一个封装了数据以及对这些数据进行操作的代码的逻辑实体
class Student
{
private:
lont no;
char name[10];
float score;
public:
Student();
void findNumber(long no);
void findName(char name[]);
void print();
}
- C++中函数成员和数据成员会根据不同要求设置不同权限,这样就对数据起到了保护作用,而且这些函数只能被本类对象调用,这样的话不同的类中拥有同一个函数名也是完全没问题的。而C中使用结构体进行封装的,函数和结构体是分开的,可以说C中的函数是对外公开的,如果在C中有多个结构体都需要打印功能,就要防止相同名字的打印函数出现
//C:结构体
typedef struct Student
{
long no;
char name[10];
float score;
};
void findnumber(long no);
void findname(char name[]);
void Print();
2. 类与对象
- 类和对象的关系:类是对一组具有共同属性特征和行为特征的对象的抽象,并将相关数据和数据的操作组合在一起。而对象是将抽象的类的具体
- 类的定义格式
class 类名
{
private:
数据成员或成员函数
protected:
数据成员或成员函数
public:
数据成员或成员函数
};
- 访问权限:C++提供三种不同的访问权限符,它规定类中说明的成员的访问属性,是C++实现封装的基本手段
1)public:表示这个成员可以被该类对象处在同一作用域内的任何函数使用
2)private:表示这个成员只能被它所在的类中的成员函数&该类的友元函数使用
3)protected:表示这个成员只能被它所在类**&该类派生的子类的成员函数&友元**函数使用
-默认是private
-数据成员可以是任意类型数据,但不能是 自动(auto)、寄存器(register)、或外部(extern)类型
-由于类是一种数据类型,系统并不会为其分配内存空间,所以不能在类生命中给数据成员赋初值
3.成员函数的定义
一般代码量较少的成员函数可以在类中定义,而代码量较大的成员函数通常在类中进行函数原型的声明,在类外对函数进行定义
返回类型 类名::函数名(参数表)
{
//函数体
}
如果在类外定义成员函数,则应该在所定义的成员函数名前加上类名,在类名和函数名之间应加上作用域运算符**“::”**,它说明成员函数从属哪个类。
- 对象的定义与使用
声明了类只是定义了一个新的数据类型,只有定义了类的对象,才能真正创建这种数据类型的实体。对象是封装了数据结构以及可以施加在这些数据结构上的操作的封装体,是具体的。C++一个具体的对象称为类的实例
有两种定义对象的方法:
1)在声明类的同时,直接定义对象,这生成的是一种全局对象,在它的生存期内任何函数都可以使用它,一直到整个程序运行结束
class Book
{
private:
char title[20],author[10],publish[30];
float price;
public:
void Input();
void Print();
}b1,b2; //表示定义b1,b2是Book类的对象。
2)先声明类,在使用的时候再定义对象:类名 对象名列表 ,一般则个方法比较多,除非要全局变量
对象的使用:使用对象就是向对象发送消息,请求执行它的某个方法,熊二向外界提供所要求的服务,形式如下:对象名.成员函数名(实参表)
3)在使用对象赋值语句进行赋值语句时,两个对象的类型必须相同,如果类中存在指针,则不能简单的将一个对象的值赋给另一个对象,否则会产生错误
3. 析构函数和构造函数
构造函数的功能是在创建对象时,给数据成员赋值,即对象的初始化。
析构函数的功能是释放一个对象,在对象删除之前,用它来做一些内存释放等清理工作
- 构造函数
类名::类名(形参列表)
{
//函数语句;
}
构造函数的特点:
1)构造函数名字必须与类名相同
2)构造函数可以有任意类型的参数,但是没有返回值类型,也不能指定为void类型
3)定义对象的时候,系统会自动调用构造函数,就是给对象一个初始化
4)如果没有定义构造函数,系统会自动生成一个默认的构造函数,只负责对象的创建,不做初始化工作
5)构造函数可以重载
- 析构函数: ~类型();
析构函数的特点:
1)析构函数没有参数,也灭有返回值,而且不能重载
2)一个类中有且仅有一个析构函数,且应该为public
3)析构函数的功能是释放对象所占用的内存空间
4) 析构函数与构造函数两者的调用次序相反,即最先构造的对象最后被析构,最后构造的对象最先被析构
5)如果一个对象被定义在一个函数体内,则当这个函数体结束时,该对象的析构函数会被自动调用
4. 内存的动态分配★★★
用户存储空间分为三个部分:程序区(代码区)、静态存储区(栈区)、动态存储空间(堆区)。
代码区存放程序代码,程序运行前就分配存储空间。
栈区存放局部变量、函数参数、函数返回值和临时变量
堆区是程序空间中存放着的一些空闲存储单元,这些空闲存储单元组成堆 ,在堆中创建的数据对象为堆对象,当创建对象时,堆中的一些存储单元从未分配状态变为已分配状态;当删除所创建的堆对象时,这些存储单元从已分配状态又编程未分配状态。
在C++中使用new
和delete
来实现在堆内存区中进行数据的动态分配和释放,在程序运行过程中申请和释放的存储单元称为堆对象
- new运算符,new出一个地址
1)new运算符的三种使用形式
指针变量 = new T;
指针变量 = new T(初始值列表);
指针变量 = new T[元素个数];
T是数据类型名,表示在堆中建立一个T类型的数据
int *p;
float *p1;
p = new int(100); //让p指向一个类型为整型的堆地址,该地址中存放数值100
p1 = new float; //让p1指向一个类型为实型的堆地址
★★★ p = new int(100); //让p指向一个类型为整型的堆地址,该地址中存放数值100
★★★ p1 = new float; //让p1指向一个类型为浮点型的堆地址
2)用new创建对对象的格式:
类名* 指针名 = new 类名(构造函数参数列表)
Complex *c1 = new Complex(1.1,2.2);
new返回一个指定的合法数据类型的内存空间的首地址,若分配不成功,则返回一个空指针
3) new 可以为数组动态分配空间,这时应该在类型名后面指明数组大小。其中,元素个数是一个整型数值,可以是常数也可以是变量
int n ,*p;
cin >> n;
p = new int[n];
4)new不能对动态分配的数组存储区进行初始化~~
5)用new分配的空间,使用结束后只能用delete显式的释放,否则这部分空间将不能回收,从而造成内存泄漏。
- delete运算符
1)delete运算符用来释放动态变量或动态数组所占用的内存空间,
格式:delete 指针变量名
释放指针所指向的动态内存空间
或delete [] 指针变量名
释放为数组动态分配的内存
2)new和delete需要配套使用
3)在用delete释放指针所指向的空间时,必须保证这个指针所指向的空间是用new申请的,并且只能释放一次,否则将产生指针悬挂问题
4)当delete用于释放由new创建的数组的连续内存空间时,无论是一维数组还是多维数组,指针变量名前必须使用[],且[]内没有数字
举例:
#include <iostream>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int a, int b);
~Point();
};
Point::Point(int a, int b)
{
cout << "constructor..." <<endl;
x = a;
y = b;
}
Point::~Point()
{
cout << "destructor..." <<endl;
}
int main()
{
Point *p = new Point(1, 3);
delete p;
return 0;
}
5. 对象数组和对象指针
一,对象数组
数组元素可以是基本数据类型的数据,也可以是用户自定义数据类型的数据。
对象数组是指每一个数组元素都是对象的数组
对象数组的元素是对象,它不仅具有数据成员,而且还有函数成员。
- 声明对象数组的形式:
类名 数组名[下标表达式];
与基本类型的数组一样,在使用对象数组时也只能引用单个数组元素。通过对象,可以访问它的公有成员。 - 对象数组的引用形式:
数组名[下标].成员函数;
- 例子1:
#include <iostream>
using namespace std;
class Circle
{
private:
double radius;
public:
Circle(double r);
double Area();
~Circle();
};
Circle::Circle(double r)
{
cout << "construct..." <<endl;
radius = r;
}
double Circle::Area()
{
return 3.14*radius*radius;
}
Circle::~Circle()
{
cout << "destruct..." <<endl;
}
int main()
{
Circle c[3] = {1,3,5};
int i;
for(i = 0; i < 3; i++)
{
cout <<c[i].Area() <<endl;
}
return 0;
}
- 例子2
#include <iostream>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int a, int b);
void Print();
};
Point::Point(int a, int b)
{
x = a;
y = b;
}
void Point::Print()
{
cout << "(" << x << "," << y << ")" <<endl;
}
int main()
{
Point a[3] = {Point(1,2), Point(3,4), Point(5,6)};
int i;
for(i = 0; i < 3; i++)
{
a[i].Print();
}
return 0;
}
二,对象指针 ★★★
访问一个对象可以通过对象名访问,也可以通过对象地址访问。对象指针就是用于存放对象地址的变量。
对象指针遵循一般变量指针的各种规则
- 声明对象指针的一般语法形式:
类名* 对象指针名;
- 使用对象指针也可以访问对象的成员:
对象指针名->成员名
- 与一般变量指针一样,对象指针在使用前必须先进行初始化。可以让它指向一个已经声明过的对象,也可以用new运算符动态建立堆对象
Circle *c1,c(3);//对象指针和一个对象
c1 = &c;//c1指向对象c
c1->Area();//c.Area()对象访问函数,c1->Area()对象指针访问函数
Circle *c2 = new Circle(3);//用new动态建立
c2->Area();
Circle *c3;
c3->Area();//错误,不能使用没有初始化的对象指针
三,自引用指针this ★★★
this指针主要用在运算符重载和自引用等场合
#include <iostream>
using namespace std;
class Square
{
private:
double a;
public:
Square(double a);
double Area();
void copy(Square &s);
};
Square::Square(double a)
{
this->a = a;
}
double Square::Area()
{
return a*a;
}
void Square::copy(Square &s)
{
if(this == &s)
return;
*this = s;
}
int main()
{
Square s1(3),s2(5);
cout << "before copy" <<endl;
cout << "s1 area is " << s1.Area() <<endl;
cout << "after copy" <<endl;
s1.copy(s2);
cout << "s1 area is " << s1.Area() <<endl;
}
👀
void Square::copy(Square &s)
{
if(this == &s)
return;
*this = s;
}
👀
Square s1(3),s2(5);
s1.copy(s2);
定义对象s1时通过构造函数将其数据成员初始化为3,当程序执行s1.copy(s2)
时,对象s1调用函数copy(),this指针指向s1自己,在copy函数中判断是不是对象在给自己赋值,如果时就不用了就直接返回,如果不是那就将传给形参s的值赋给this自己就是对象s1
👀
Square::Square(double a)
{
this->a = a;
}
这边this->a = a
是将传给形参的值传给自己的对象成员a,但是都是a,所以要使用this指针告诉编译器是把参数a传给私有成员a
使用this指针应该注意:
1)this指针是一个const指针,不能在程序中修改或给它赋值
2)this指针是一个局部数据,它的作用域仅在一个对象的内部
3)静态成员函数不属于任何一个对象,所i在静态成员函数中没有this指针
6. 函数参数的传递机制
C中函数的参数传递有两种方式:按变量值传递和按变量的地址传递
C++中,不仅可以实现C的两种还可以进行引用传递,不仅简单变量可以作为参数进行传递,对象也可以作为参数传递给函数,其方法与传递其他类型的数据一样
1.使用对象作为函数参数
#include <iostream>
#include <iomanip>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int a, int b);
void Add(Point p);
void Print();
};
Point::Point(int a, int b)
{
x = a;
y = b;
}
void Point::Print()
{
cout << "x:" << x << setw(5) << "y:" << y <<endl;
}
void Point::Add(Point p)
{
p.x = p.x + 1;
p.y = p.y + 1;
}
int main()
{
Point ob(1,2);
cout << "before add:";
ob.Print();
ob.Add(ob);
cout << "after add:";
ob.Print();
return 0;
}
在执行Add函数时,由于作为参数的对象是按值传递的,也就是实参ob将自己的值对应地赋给形参p,在Add函数中对形参的数据成员x,y值的修改结果并没有传回主程序,所以结果是没有变化的👇👇
2.使用对象指针作为函数参数
#include <iostream>
#include <iomanip>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int a, int b);
void Add(Point *p);
void Print();
};
Point::Point(int a, int b)
{
x = a;
y = b;
}
void Point::Print()
{
cout << "x:" << x << setw(5) << "y:" << y <<endl;
}
void Point::Add(Point *p)
{
p->x = p->x + 1;
p->y = p->y + 1;
}
int main()
{
Point ob(1,2);
cout << "before add:";
ob.Print();
ob.Add(&ob);
cout << "after add:";
ob.Print();
return 0;
}
对象指针作为参数传递给函数,就是传地址,即形参和实参共享的是同一内存单元,因此作为形参的兑现,其数据的改变将影响实参对象,从而实现函数之间的信息传递。此外,**使用对象指针作为参数仅将对象的地址传递给形参,而不进行副本的复制,**可以提高运行效率,减少时空开销
3.使用对象应用作为函数参数
引用相当于变量的别名
1)声明引用的格式:
数据类型 &引用名 = 已定义的变量名;
int &j = i;//表示j是i的引用,变量i和j对应同一内存单元,有一个变了另一个就变了
Point &p = ob;//ob是一个已经定义的Point类的对象,对象p是对象ob的引用
#include <iostream>
#include <iomanip>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int a, int b);
void Add(Point &p);
void Print();
};
Point::Point(int a, int b)
{
x = a;
y = b;
}
void Point::Print()
{
cout << "x:" << x << setw(5) << "y:" << y <<endl;
}
void Point::Add(Point &p)
{
p.x = p.x + 1;
p.y = p.y + 1;
}
int main()
{
Point ob(1,2);
cout << "before add:";
ob.Print();
ob.Add(ob);
cout << "after add:";
ob.Print();
return 0;
}
在调用Add(Point &p)
函数时,里面形参类型时引用型,但是传入ob是可以的ob.Add(ob);
,但是如果定义一个Point &yy =ob
,然后往Add(Point p)
传入yy就是错误的。
三种传递方式比较
- 使用对象作为函数参数:当进行函数调用时,需要给形参分配存储单元,形参和实参的结合是值传递,实参将自己的值传递给形参,形参实际上是实参的副本,这是一种单向传递,形参的变化不会影响到实参
- 使用指针作为函数参数:实参将自己的地址传递给形参,这是一种双向传递,形参的变化会直接影响到实参
- 使用引用作为函数参数:当进行函数调用时,在内存中并没有产生实参的副本,它是直接对实参操作的,这中方式是双向传递,形参的变化会直接影响到实参。与指针做函数参数比较,这种方式更容易使用,更清晰,而且当参数传递的数据较大时,用引用比用一般传递参数的效率和所占空间都好
7. 友元
友元函数不是当前类中的成员函数,**它可以是一个不属于任何一个类的一般函数,也可以是另一个类的成员函数。**当函数被声明为一个类的友元函数后,它就可以通过对象访问类的私有成员和保护成员。
非成员函数作为友元函数,其作为类的友元函数后,就可以通过对象访问封装在类内部的数据。
声明友元函数: friend 函数返回值 函数名(形参表);
例子👇
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(double r, double i);
void Print();
friend Complex add(Complex c1, Complex c2);
};
Complex::Complex(double r, double i)
{
real = r;
imag = i;
}
void Complex::Print()
{
cout <<real;
if(imag > 0)
cout << "+";
if(imag != 0)
cout << imag << "i" <<endl;
}
Complex add(Complex c1, Complex c2)
{
c1.real = c1.real + c2.real;
c1.imag = c1.imag + c2.imag;
return c1;
}
int main()
{
Complex com1(1.1, 2.2),com2(3.3, 4.4);
com1 = add(com1, com2);
com1.Print();
return 0;
}
(1)友元函数定义的时候,不用在函数面前家“类名::”,因为它不是该类的成员函数;友元函数没有this指针;调用友元函数时必须在它的实参表中给出要访问的对象。
(2)友元函数可以在类的私有部分进行声明,也可以在类的公有部分进行声明。
(3)当一个函数需要访问多个类时,应该把这个函数同时定义为这些类的友元函数。
(4)友元函数可以使函数能够访问某个类中的私有或保护成员。如果类A的所有成员函数都想访问类B的私有或保护成员,一种方法是将类A的所有成员函数都声明为类B的友元函数,但是这样做显得比较麻烦,且程序也显得冗余。为此,C++提供了友元类。也就是一个类也可以作为另一个类的友元类。若类A声明为类B的友元类,则类A中的每个成员函数都具有访问类B的保护或私有数据成员的特权。
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(double r, double i);
friend class Vector;
};
class Vector
{
private:
double x,y;
public:
void Change(Complex c);
void Print(Complex c);
};
void Vector::Change(Complex c)
{
x = c.real;
y = c.imag;
}
void Vector::Print(Complex c)
{
cout << "fushu:";
cout << c.real;
if(c.imag > 0)
cout << "+";
cout <<c.imag << "i" <<endl;
cout << "Vector:";
cout << "(" << x << "," << y << ")" <<endl;
}
Complex::Complex(double r, double i)
{
real = r;
imag = i;
}
int main()
{
Complex c(1.1, 2.2);
Vector v;
v.Change(c);
v.Print(c);
return 0;
}
friend class Vector;
友元关系是单向的,且友元关系不能传递