文章目录
前言
前一章我们对面向对象的程序设计(基于对象的程序设计)有了一个基本的了解,知道了面向对象的两个特征:抽象和封装。后面我们将继续学习面向对象的程序设计(基于对象的程序设计)的基础知识,只有大号基础,才能深入学习c++。
一、构造函数、析构函数
1.构造函数(数据成员初始化)
之前的编程过程中,我们发现给对象初始化是一个很困难的事情,因此C++为了解决这个问题,提供了构造函数来解决这个问题,同时构造函数是public,可以在类内定义构造函数,也可以在类内声明类外定义构造函数。再说一点,实参不能被用户显示的调用,它是定义对象的时候自动调用,如stud.Student()是错误的。
①无参构造函数:和类名同名,没有函数类型和返回值,默认情况下自动会有一个隐式的无参构造函数,在我们定义对象的时候调用,只不过里面什么都没做,有点浪费,现在我们要在里面搞点事情–对象初始化。
②有参的构造函数:
1)赋值语句
类名 构造函数名(类型1 形参1,类型2 形参2,…)
{
赋值语句;
其他语句;
}
在对象定义的时候: 类名 对象名(实参1,实参2,…)
2)参数初始化表
类名 构造函数名(类型1 形参1,类型2 形参2,…):成员初始化表
{
其他剩余赋值语句;
其他语句;
}
③构造函数的重载
和函数的重载原理一致,函数名字相同,但是参数的个数或者参数的类型不同,在建立对象时,系统自动找到与之对应的构造函数
默认的构造函数:可以不用参数而调用的构造函数
④使用默认参数的构造函数(这一点用的时候要小心,注意和③不能混合用,反正我是没有搞明白,貌似分为全部都是默认参数和部分默认参数!!!!)
它的作用相当于好几个重载的构造函数,它的好处:即使在调用的时候没有提供实参,不仅不会出错,而且还会按照默认的参数值对对象进行初始化。
定义方法:
在类声明时就进行函数声明时就给出默认值:Box(int h = 10, int w =10, int len =10);因为类 声明时放在头文件中的,是对外的接口,用户一眼就看到,而函数定义时用户看不到的Box ::Box(int h , int w, int len){构造函数体};,这样用户在使用构造函数定义对象的时候就知道使用的默认值是什么。
constructor.h
#ifndef __CONSTRUCTOR_H__
#define __CONSTRUCTOR_H__
/**
*
*/
class Box
{
public:
//声明一个无参的构造函数,在外部定义函数
Box();
//定义一个有参的构造函数,使用的时参数的初始化表对数据成员初始化,在外部就不能再定义了,
//也可以在这里做声明,外部向这样定义,好像高手都是这么玩的,所以我们要看懂
//否则出现function '__thiscall Box::Box(int,int,int)' already has a body
//Box(int h, int w, int len):height(h), weight(w), length(len){};
//声明一个有参的构造函数,所有的参数都有默认值,这个就需要考虑是否包含上面两个情况,
//不然编译的时候会出现模糊调用ambiguous call to overloaded function
//multiple default constructors specified
Box(int h = 10, int w = 10, int len = 10);
int volume();
private:
int height;
int weight;
int length;
};
#endif
constructor.c
#include "stdafx.h"
#include "constructor.h"
Box::Box()
{
height = 11;
weight = 11;
length = 11;
}
Box::Box(int h, int w, int len)
{
height = h;
weight = w;
length = len;
}
int Box::volume()
{
return (height * weight * length);
}
main.c
#include "stdafx.h"
#include "constructor.h"
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
//调用的时默认参数的构造函数
Box box(2);
cout << box.volume();
return 0;
}
2.析构函数(在撤销对象所占内存之前的一些任何操作)
有人说是和构造函数相反,我觉得他俩压根就不是一回事,析构函数时在对象生命周期结束时,程序自动调用的函数,这个就涉及到对象的作用域啊、局部的还是全局的,是动态的还是静态的等等,和变量是一个道理。总之对象生命周期结束才自动调用。而且一个类只有一个,没有函数参(所以不能被重构)、没有返回值,同时是public,定义的格式:~析构函数名()–析构函数名、构造函数名和类名都叫一个名字。
destructor.h
#ifndef __DESTRUCTOR_H__
#define __DESTRUCTOR_H__
/**
*
*/
class Pencil_Box
{
public:
//声明一个无参的构造函数,在外部定义函数
Pencil_Box();
//定义一个有参的构造函数,使用的时参数的初始化表对数据成员初始化,在外部就不能再定义了,
//也可以在这里做声明,外部向这样定义,好像高手都是这么玩的,所以我们要看懂
//否则出现function '__thiscall Pencil_Box::Pencil_Box(int,int,int)' already has a body
//Pencil_Box(int h, int w, int len):height(h), weight(w), length(len){};
//声明一个有参的构造函数,所有的参数都有默认值,这个就需要考虑是否包含上面两个情况,
//不然编译的时候会出现模糊调用ambiguous call to overloaded function
//multiple default constructors specified
Pencil_Box(int h = 10, int w = 10, int len = 10);
//声明析构函数,在外部定义函数
~Pencil_Box();
int volume();
private:
int height;
int weight;
int length;
};
#endif
destructor.c
#include "stdafx.h"
#include "destructor.h"
#include <iostream>
using namespace std;
Pencil_Box::Pencil_Box()
{
height = 11;
weight = 11;
length = 11;
}
Pencil_Box::Pencil_Box(int h, int w, int len)
{
height = h;
weight = w;
length = len;
}
Pencil_Box::~Pencil_Box()
{
cout << "Pencil_Box is over!" << endl;
}
int Pencil_Box::volume()
{
return (height * weight * length);
}
main.cpp
#include "stdafx.h"
#include "constructor.h"
#include "destructor.h"
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
//调用的时默认参数的构造函数
Box box(2);
//调用的时默认参数的构造函数
Pencil_Box pencil_box(2);
cout << box.volume();
return 0;
}
二、对象的数组、对象的指针
1.对象的数组
和普通的变量的数组一样,此时数组中的远元素是一个一个的对象,例如定义一个学生类的对象数组,如果有N个元素就调用N次构造函数,对象数组的初始化有两种方式:
①如果构造函数的实参只有一个,则在定义数组时直接在等号后面的花括号内提供实参:
Student stud[3] = {12, 13 ,14};
但是如果构造函数有多个参数,则不能用这种方法,编译器不知道怎么处理,上述代码中,编译系统会将3个参数分别作为3个元素的第一个实参。
②如果构造函数有多个参数,则需要采用以下的方式:
Student stud[3] = {
Student(1001, 18 ,87),
Student(1002, 18 ,76),
Student(1003, 20 ,87),
};
每个元素分别调用一次构造函数,对每一个元素进行初始化,每个元素的实参分别括号括起来,对应构造函数的一组形参,不会混淆。
//对象的数组
int main(int argc, char* argv[])
{
// //调用两次默认的无参构造函数
// Box box[2];
//调用两次有参的构造函数
Box box[2] = {
Box(50, 52),
Box(10, 11, 12)
};
cout << box[1].volume();
return 0;
}
2.对象的指针
类比结构体的指针。
①指向对象的指针
定义:类名 * 对象指针名
可以使用对象加成员运算符(.)访问对象成员,也可以使用对象指针加指向运算符(->)访问对象成员
②指向对象成员的指针(指向对象数据成员的指针和指向对象成员函数的指针)
1)指向对象数据成员:就是普通变量的方式
int *p1; //指向整形数据的指针变量
p1 = &t1.hour;//将对象t1的数据成员hour的地址赋给p1,p1指向t1.hour
cout << *p1 << endl;
2)指向对象成员函数:和普通函数的指针变量不同
普通函数指针的定义:类型名 (*指针变量)(参数列表)
指向对象成员函数(公有)指针的定义:类型名 (类名:: 指针变量名) (参数列表)
指针变量名 = &类名::成员函数名
调用成员函数:(对象名.*指针变量名 ) (参数列表)
void (Time::*p2)();//定义一个指向公有成员函数的指针
p2 = &Time::get_time;//将公有成员函数地址赋值给指针变量
(t1.*p2)();//调用成员函数函数!!!!!!!
③this指针的精髓
this指针时隐式使用的,它是作为参数被传递给成员函数的,本来函数volume的定义是:
int Box::volume()
{
return (height * width * length);
}
a.volume();
其实C++把它做了如下处理,即在成员函数的形参表列中增加一个this指针,函数将对象的起始地址赋给this,这些是编译系统自动实现的。
int Box::volume(Box *this)
{
return (this->height * this->width * this->length);
}
a.volume(&a);//调用成员函数时其实内部的机制时这样的
④指针的例子
main.cpp
//对象的指针
int main(int argc, char* argv[])
{
//使用有参的构造函数定义一个对象
Time t1(12,30,36);
//调用成员函数时其实内部的机制时这样的t1.get_time(&t1)
t1.get_time();
//定义一个对象的指针,并将对象的起始地址赋值给指针p1
Time *p1 = &t1;
//调用p1所指向对象的成员函数get_time
p1->get_time();
//定义一个指向数据成员的指针,并将数据成员hour的地址赋值给p2
int *p2 = &t1.hour;
cout << *p2 << endl;
//定义一个指向成员函数get_time的指针,并将成员函数get_time的地址赋值给p3
void (Time::*p3)() = &Time::get_time;
//调用t1中p3所指向的成员函数!!!!!
(t1.*p3)();
return 0;
}
Time.cpp
#include "stdafx.h"
#include "Time.h"
#include <iostream>
using namespace std;
//定义一个有参的构造函数
Time::Time(int h, int m, int s){
hour = h;
minute = m;
sec = s;
}
//定义一个公有的成员函数
void Time::get_time(){
cout << hour << ":" << minute << ":" << sec << endl;
}
void Time::set_time(){
;
}
Time.h
#include "stdafx.h"
#include "Time.h"
#include <iostream>
using namespace std;
//定义一个有参的构造函数
Time::Time(int h, int m, int s){
hour = h;
minute = m;
sec = s;
}
//定义一个公有的成员函数
void Time::get_time(){
cout << hour << ":" << minute << ":" << sec << endl;
}
void Time::set_time(){
;
}
⑤指针的总结
其他的也没有什么好总结的,主要是成员函数的指针,这个有必要好好说一说,我们都知道成员函数不是放在对象的空间中的,而是放在对象外的空间中,是所有对象公用的一个函数代码段,所以给他赋值的时候不能使用如下:指针变量名 = &对象名.成员函数名,结合this指针,我们知道对象名.成员函数名,仅仅就是调用函数而已,对象也不知道成员函数的地址,所以&对象名.成员函数名给成员函数指针变量行不通。但是调用的时候需要这样做::(对象名.*指针变量名 ) (参数列表),而不能这样做:(*指针变量名 ) (参数列表),因为调用的时候需要告诉他对象的地址(记住这么用既可以!!!)。
三、共用数据的保护
尽管使用了private保护,可以有效的预防在类外对对象的私有成员进行操作,但是除了以上的一些情况,还有其他的场景需要注意,c++针对其他的场景设置了专门的语句供大家是使用,但是要注意和static的结合使用。
1.常对象(在定义对象的时候对所有的数据成员作了限制)
①仅仅是在定义对象的时候做const限制,不是声明类的时候!!!,而且定义对象的时候必须初始化,之后在对象的生命周期内所有的数据成员不能改变,定义方式如下:
类名 const 对象名[(实参表)];
或者 const 类名 对象名[(实参表)];
②如果一个对象被定义成常对象,则对外使用的接口函数只能是其中的常成员函数(在类声明的时候加上const修饰符的函数:如void get_time() const;)、系统自动调用的隐式构造函数(这里包括自己定义的构造函数和析构函数,注意和默认构造函数的区别,这里的意思是explicit关键字)和析构函数以及静态成员函数,其他的普通函数在外面都不能用了;
③编译系统因为无法确认在普通成员函数中是否对常对象的成员数据进行修改,以此编译系统的做法是只要在外面调用普通成员函数,就直接不能调用;
④常成员函数也仅仅能访问(引用)常对象中的数据成员,不能修改他们(听说如果想要修改常对象中的某个数据成员,只需要在某个数据成员前面加上修饰符mutable,这样常成员函数既可以访问这个数据成员,也可以修改它)。
2.常对象成员(在声明类的时候对某些数据成员和成员函数作了限制)
①常数据成员:在类的声明中对某些数据成员使用const进行限制,这样这个数据成员就和常变量一样了,注意:对常数据成员进行初始化时只能使用构造函数参数初始化表进行赋值(初始化),其他的普通构造函数都不行,因为他们使用的时赋值语句!!!,如果不这样编译的时候会报错:must be initialized in constructor base/member initializer list
②常成员函数:类型名 函数名(参数表)const;
1)在定义和声明的时候都需要加上const;
2)常对象只保证所有的数据成员都是const的,编译器默认将常对象的成员函数处理为非const的;
3)const和非const函数 是否可以引用和修改 const对象(所有数据成员都是const)和非const对象(数据成员有const的,也有非const的),这个不是很清楚!!,需要后续继续学习补充,先死记住用法!
4)const函数的本质:
const成员函数的写法有两种
1、void fun(int a,int b) const{}
2、void const fun(int a,int b){}
这两种写法的本质是:void fun (const 类 *this, int a,int b);
const修饰的不是形参a和b;const修饰的是属性this->a和this->b。与const所写的位置无关。为什么?因为c++对类的this指针做了隐藏,本质上,const指针修饰的是被隐藏的this指针所指向的内存空间,修饰的是this指针。
//类的成员函数可通过const修饰,请问const修饰的是谁
#include<iostream>
using namespace std;
class test
{
public:
//1.const写在什么地方没关系 void const oper(int a,int b)==void oper(int a,int b) const
//为什么没有关系呢?
// 2:请问 const修饰的是谁
//const修饰的是形参a吗?可以在代码中加入a=100,编译通过,说明不是a;
//const修饰的是属性this->a。this->b吗?,在代码中加入this->a=100,或者this->b=100,编译器会报错
//但是感觉不精确,如果修饰的是this->a,this->b,const为什么不写到括号里面呢。
//由以前的内容可知,非静态函数都默认有一个this指针,
//所以总而言之,const修饰的是this指针,void const oper(const test*this,int a,int b)
//意思就是就是说指针所指向的内存空间不能被修改,所以this->a,this->b不能被修改。
void oper(int a,int b) const//<==>void const oper(test *this,int a,int b)
{
cout<<"a"<<a<<endl;
cout<<"b"<<this->b<<endl;
}
private:
int a,int b;
};
int main()
{
cout<<"hello"<<endl;
return 0;
}
//非const成员函数只可以访问非const对象的任意的数据成员(不能访问const对象的任意数据成员);
//(上述原因可详见C++Primer(5th)231页。 在默认情况下,this的类型是指向类类型非常量版本的常量指针,
//例如 Screen类中,this类型为 Screen *cosnt。当在成员函数的后面加上const关键字时,隐式的将this指针修改为 const Screen *const
//即指向类类型常量版本的常量指针。根据初始化原则,我们不能将一个常量指针赋值给一个非常量指针)
3.指向对象的常指针
①定义:类名 *const 指针变量名;
②含义:const修饰的是指针变量,所以指针变量的值不能改变,即始终指向同一个对象,对象的值可以改变,这个和它无关。
③用法:一般用在函数中,函数形参定义成这样,此时形参就是一个常指针,目的在于保护形参指针的值不能改变,如指针++等操作都不行。
4.指向常对象的指针
①定义:const 类名 * 指针变量名;
②含义:const修饰的是指针指向的对象,所以不能通过指针修改对象的值,但是指针的值可以改变。
③用法:一般用在函数中,函数形参定义成这样,此时形参就是一个指向常对象的普通指针,目的在于保护形参指针所指向的对象,是其在函数的执行过程中不被修改。
④注意事项:
赋值给指向常对象的指针可以是普通对象的地址,也可以是常对象的地址,如果是普通对象的地址,则此时是不能通过指针访问对象内非const函数,因为通过指针访问对象期间,对象具有常对象的特征,其他情况下,对象仍然是一个普通的对象;
赋值给指向普通对象的指针只能是普通对象的地址;
这其实是编译器不智能的体现,它是从根上断绝我们乱用,导致数据错乱,包括之前的const函数等也是一样的。我们细细体会。
5.指向常对象的常指针(前面两者的结合)
6.指向对象的常引用
①定义:void fun(const Time &t);
②含义:由之前引用的含义可以知道。
③用法:一般用在函数中,函数形参定义成这样,这里目的在fun函数中,不能修改t的值,也就是实参的值不能修改,因为他们是指向同一片地址。
④注意事项:常指针和常引用在C++编程中比较常见,因为他们在时间和空间上节约了很多,我们需要经常使用,熟能生巧!
7.指向常对象的引用
8.指向常对象的常引用
四、对象的动态建立(new)和释放(delete)
之前我们定义的对象不能自由的撤销,因为他是静态的,无论是全局的还是局部的,只有在函数或者是程序结束是才释放,和全局变量、局部变量一致,我们可以使用new和delete动态的创建和撤销一个对象,new返回的是一个对象的指针,如果内存不够,则返回NULL,而且这个对象没有名字,只能通过地址访问,所以就定义一个指针来接收他,之后通过指针来访问他,和malloc、free一致,要注意内存溢出和内存泄漏。
Box *pt;
pt = new Box;//自动调用构造函数
delect pt;//自动调用析构函数
五、对象的赋值和复制
1.对象的赋值
如果同一个类定义了两个或者多个对象,则对象之间可以相互赋值,使用的是重载的赋值运算符“=”,注意只有数据成员赋值: 对象1 = 对象2;
2.对象的复制
如果想用一个存在在对象产生一个新的对象,则可以使用对象的赋值,其原理是使用复制构造函数(copy constructor function),它和普通的构造函数类似,系统根据参数的类型自动调用,系统会有一个,像无参的构造函数一样。
①语法:
1)类名 对象2(对象1);
2)类名 对象2 = 对象1;
以上两个都是调用复制构造函数。
②普通构造函数和复制构造函数的区别:
1)在函数声明和定义上:
类名(形参表列);
类名(形参表列){
}
类名(类名 &对象名);
类名(类名 &对象名){
}
2)在调用上:
Box box1(12,13,14);
Box box2(box1);
3)什么时候被调用:
普通构造函数:在建立对象时被调用;
复制构造函数(3种情况下):
i)程序中需要建立新对象,并用另一个同类的对象对它初始化,之前都是这种情形;
ii)当参数的对象为类对象时;
iii)当函数的返回值时类对象时。
以上情况,系统自动参与,我们人为不用干预,就类似this一样,我们需要知道它背后的机理即可。
六、静态成员(静态数据成员和静态成员函数)
1.静态数据成员
静态数据成员是实现类和类对象之间的数据沟通,是所有类以及类对象公有的,和对象不在同一空间,不随对象的消失而消失,如以下类声明:
class sBox
{
public:
sBox(int, int);
int volume();
static int height;
private:
int width;
int length;
};
注意注意注意:类内的静态变量仅仅是个声明而已,也就是说height还不存在,需要在cpp实现中加上这么一句:
int sBox::height = 10;//也可以什么都不加int sBox::height;
这才是真正的定义height变量,而不随对象的定义而定义,这样就有了静态变量height,其他的操作就和普通的数据成员一样,public和private同样适用,唯一的区别就是,如果height是public的,则可以使用两种方式引用它,
1)sBox::height;
2)对象名.height。
2.静态成员函数
静态成员函数和非静态的成员函数之间的区别就是:
①非静态的成员函数有默认的this指针,因此它就可以调用静态数据成员,也可以调用非静态的数据成员;
②而静态的成员函数没有,因此它调用非静态的数据成员(对象特有的)有点费劲,需要好好处理一下,c++中好的习惯是静态的成员函数只引用静态的数据成员,我们要向好的习惯看齐;
和静态成员函数一样,则可以使用两种方式引用静态成员函数,
1)类名::静态成员函数;
2)对象名.静态成员函数。
3.内部机制!!!!
类中的静态数据成员仅仅声明,所以可以有如下的语句:
class sBox
{
public:
sBox(int, int);
int volume();
//其实是告诉编译系统,这个变量在其他地方有定义,和extern int height
//类似,所以后面int sBox::volume()可以编译通过,
static int height;
static int get_h();
private:
int width;
int length;
};
//这里才是定义
int sBox::height;
//这里其实是这样的机制
//int sBox::volume(sBox *this)
//{return (height * this->width * this->length);}
int sBox::volume(){
return (height * width * length);
}
//所以以下的函数是有问题的,因为他不知道width是什么
int sBox::get_h(){
return (height * width);
}
七、友元和类模板
1.友元(friend)
之前的说明中,我们知道类中分为公有的(public)和私有的(private),在类外可以访问公有的成员(公有数据成员和公有的成员函数),只有本类中的函数才可以访问私有成员(私有数据成员和私有的成员函数),现在我们介绍一种例外–友元,他也可以访问其他类的私有成员,但是在其他类中要将他声明成friend,打个比方,一般的来客只能在客厅活动,但是亲密的朋友可以去书房等私有地方活动,但是首先主人需要将他声明为朋友才可以。
①友元函数:可以是普通函数,也可以是类中的成员函数(貌似是要public的)
用法:在主人的类中声明如下:friend 函数原型;
注意这里涉及到类的提前引用声明,这个没用过,知道这么个概念!
②友元类:直接在其B类声明成A类的友元类,就类似将朋友的一家人声明成朋友,这样就都可以进入主人书房。
用法:在主人的类中声明如下:friend 类名;
2.类模板(这个貌似在实际中用的比较多)
对于功能一致,数据类型不同的类,实现一类多用,如果说类时对象的抽象,对象时类的实例,那么类模板就是类的抽象,类是类模板的实例。在定义对象的时候,编译系统会将实际的类型替换类模板中的类型,从而定义对象。注意类模板的作用域,以代码最后完整的模块为结尾,如这个整个函数,完整的类声明等。
声明和使用类模板:
①先写出一个实际的类;
②将此类中准备改变的类型名(如int)改成自己指定的虚拟的类型名(numtype);
③在类的声明前面加入一行:template<class numtype1,class numtype2 >(备注:此处关键字class不是类的意思,表示其后面跟着的是虚拟的类型名,有多少个虚拟的类型名,就用逗号和class隔开);
④用类模板定义对象时使用以下的语句:
类模板名 <实际类型名1,实际类型名2>对象名;
类模板名 <实际类型名1,实际类型名2>对象名(实参表);
⑤如果在类模板类外定义成员函数,语法要求如下:
template<class numtype1,class numtype2 >
函数类型 类模板名<虚拟类型参数>::成员函数名(函数参数列表)
{
}
总结
这一主要是对面向对象的程序设计中一些的重要语法说明,一定要熟记于心,真正的高手肯定是数运用以上内容,保证效率的同时数据的安全性肯定考虑的很全民啊,如善于运用const等等。