文章目录
一、C++类和对象
1.c++类的定义
什么是类?什么是对象?类和对象的理解是整个语言学习中核心的基础。通俗的理解,类其实就是一个模子,是一个变量类型,对象就是这个类型定义出来的具体的变量,就像int a;这句话,int对应类,a就对应对象。这样大家应该就好理解了,但需要注意的是int是C++的内置类型,并不是真正的类。
类是对象的抽象和概括,而对象是类的具体和实例。
c ++中类的其实就是包含函数的结构体!因为C++类里面的成员除了可以像C语言的结构体那样包含基本变量以外,还可以包含函数,前者叫做成员变量,后者叫做成员方法。
关键字用class类定义,比如下面定义一个C++的类,学生类:
class Student
{
public:
int num;
char name[100];
int score;
int print()
{
cout<<num<<" "<<name<<" "<<score;
return 0;
}
};
类里还有一个public的东西,它是控制成员访问权限的一个存取控制属性,除了public以外,还有private、protected一共三种。其中private表示私有,被它声明的成员,仅仅能被该类里的成员访问,外界不能访问,是最封闭的一种权限;protected比private稍微公开一些,除了类内自己的成员可以访问外,它的子类也可以访问(关于子类的概念我们会在后面详细展开);而public声明的成员,则可以被该类的任何对象访问,是完全公开的数据。
刚才看的这种写法,成员函数是写在类里的,如果类里的成员函数很多的话,阅读起来就会乱很多,故此,C++还支持另外一种写法,就是成员函数仅在类内声明函数原型,在类外定义函数体,这样在类里可以看到所有成员函数的列表,像目录一样一目了然,规范很多。
在类中声明函数原型的方法与一般C语言的函数原型声明一样,而在类外定义函数的方法,则需要类名加::作用域限定符表示,我们还以刚才的Student类为例,类外定义的代码如下:
class Student
{
public:
int num;//学号
char name[100];//名字
int score;//成绩
int print();//类内声明print函数
};
int Student::print()//在类外定义完整的print函数
{
cout<<num<<" "<<name<<" "<<score;
return 0;
}
注意:print函数在类里声明后,我们在后面又完整的定义了出来,函数头部分在返回值和函数名之间用类名加::的方式指明这个函数是属于哪个类的。
2.C++对象的建立和使用
类是对象的抽象和概括,而对象是类的具体和实例
- 对象的创建:
类就是包含函数的结构体,是一种自定义数据类型,用它定义出来变量,就是对象,这就是所谓的“对象是类的具体和实例”,定义了一个这个类的对象,也可以说实例化了一个对象,就是这个意思!
而对象的使用,和结构体的使用也一样,都是主要访问里面的成员,也都是用过.的方式来访问,如:
Student A;
A.num = 101;
strcpy(A.name,"dotcpp");
A.score = 100;
A.print();
需要注意的是,这里类中的成员变量都是声明为public类型的,如果声明为private类型,则在主函数中主要通过对象.变量的方式直接访问的话就会被禁止报错,原因private类型的变量是私有类型,不允许外部访问。
对于想保护但又想控制的私有变量,我们通常将其声明为private类型,然后同时定义一个public类型的专门赋值的方法,由于内部成员可以访问private声明的变量,我们就可以在外部通过这个public的方法来间接控制这些私有的成员,来起到封装、保护的效果,而这个public类型的方法,也称之为这个类的一个外部接口。
- 对象的指针:
与普通变量一样,对象也是一片连续的内存空间,因此也可以创建一个指向对象的指针,即对象指针,存储这个对象的地址。
那么创建方法与使用一般类型的指针类似,定义方法如下:
类名 *指针名;
如定义Student *p;定义一个Clock类型的指针p,需要清楚的是,这里并没有建立对象,当然也不会调用构造函数。接下来就可以将一个同类型的类对象地址赋值给这个指针,然后通过->来访问对象中的成员。
代码如下:
Student *p;
Student A;
p = &A;
p->print();
以上是对象指针的使用方法,除了在赋值、访问成员的时候用以外,在传参的时候也建议用指针来传递,因为其传递的为地址,不会进行对象之间的副本赋值,从而减少内存的开销,提高效率。
- 对象的引用:
引用,**是C++中一种新的类型,对象引用就是一个类对象起个别名,本质上也是把这个类对象的地址赋给了这个引用类型,两者是指向一块内存空间的。**那么如何定义使用?下面给大家展示。
Student A;
Student &Aq=A;
如以上代码,定义一个Student类型的对象,然后用&来定义一个该类类型的引用类型,并把A对象赋给Aq作为初始化。
需要注意的是:
- 与指针一样,两者必须是同类型才可以引用。
- 除非做函数的返回值或形参时,其余定义引用类型的同时就要初始化!
- 引用类型并非是新建立一个对象,因此也不会调用构造函数。
那么既然是类对象的别名,因此使用方法也和类对象一样,用别名.成员的方法进行访问,如:
Student A;
Student &Aq=A;
Aq.print();
用引用类型时,本质还是存的地址,因此无论传参定义都不会太多内存开销,有指针的优势,同时使用起来和类对象本身使用一样,再做函数实参时,直接传入引用对象就可以,不用加地址符,因此看起来更直观、方便。这就是引用类型的优点。
3.C++中的构造函数(Constructor)
C++中有这么一种特殊的函数,它在类里,与类名同名,且没有返回值的一个函数,只要我们定义一个类的对象,系统就会自动调用它,进行专门的初始化对象用,而大多数情况下,因为我们没有定义构造函数,系统会默认生成一个默认形式、隐藏着的构造函数,这个构造函数的函数体是空着的,因此不具有任何功能。
如何定义自己的构造函数,需要用户自行定义了至少一个构造函数,系统就不在自动生成,而是根据用户定义的构造函数选择最匹配的一个进行调用。
例如还是Student类的例子,我们添加一个带有默认参数的构造函数,代码如下:
#include<iostream>
#include<Cstring>
using namespace std;
class Student
{
private:
int num;//学号
char name[100];//名字
int score;//成绩
public:
Student(int n,char *str,int s);
int print();
int Set(int n,char *str,int s);
};
Student::Student(int n,char *str,int s)
{
num = n;
strcpy(name,str);
score = s;
cout<<"Constructor"<<endl;
}
int Student::print()
{
cout<<num<<" "<<name<<" "<<score;
return 0;
}
int Student::Set(int n,char *str,int s)
{
num = n;
strcpy(name,str);
score = s;
}
int main()
{
Student A(100,"dotcpp",11);
A.print();
return 0;
}
需要注意的是,由于我们已经在类中定义了一个带默认参数的构造函数,则系统不会再自动生成,这个时候我们定义对象的时候也需要传入三个默认初始值,因为构造函数可以重载,系统会找最匹配的一个函数,但如果定义时不是带默认参数的构造函数,则会报错
4.C++中的析构函数(Destructor)
除了上一节讲到的类对象在创建时自动调用的构造函数,在对象销毁时也会自动调用一个函数,它也和类名同名**,也没有返回值,名字前有一个波浪线~,**用来区分构造函数,它的作用主要是用做对象释放后的清理善后工作。它就是析构函数。
与构造函数相同的是,与类名相同,没有返回值,如果用户不定义,系统也会自动生成一个空的析构函数。而一旦用户定义,则对象在销毁时自动调用。
我们以Student类为例,继续添加析构函数,同时在构造函数和析构函数中都添加了输出当前类的信息,用来辨别哪一个类的创建和销毁,请大家仔细阅读代码:
#include<iostream>
#include<Cstring>
using namespace std;
class Student
{
private:
int num;//学号
char name[100];//名字
int score;//成绩
public:
Student(int n,char *str,int s);
~Student();
int print();
int Set(int n,char *str,int s);
};
Student::Student(int n,char *str,int s)
{
num = n;
strcpy(name,str);
score = s;
cout<<num<<" "<<name<<" "<<score<<" ";
cout<<"Constructor"<<endl;
}
Student::~Student()
{
cout<<num<<" "<<name<<" "<<score<<" ";
cout<<"destructor"<<endl;
}
int Student::print()
{
cout<<num<<" "<<name<<" "<<score<<endl;
return 0;
}
int Student::Set(int n,char *str,int s)
{
num = n;
strcpy(name,str);
score = s;
}
int main()
{
Student A(100,"dot",11);
Student B(101,"cpp",12);
return 0;
}
看到对象A和B的构造函数的调用顺序以及构造函数的调用顺序,完全相反!原因在于A和B对象同属局部对象,也在栈区存储,也遵循“先进后出”的顺序!
5.C++拷贝构造函数实例详解
拷贝构造函数
在C++中,与类名同名,且形参是本类对象的引用类型的函数,叫做拷贝构造函数(Copy Constrctor),与构造函数一样,当我们不主动定义的时候,系统也会自动生成一个,进行两个对象成员之间对应的简单赋值,用来初始化一个对象,如以下的情形:
#include<iostream>
using namespace std;
#define PI 3.1415
class Circle
{
private:
double R;
public:
Circle(double R);
Circle(Circle &A);
double area();
double girth();
};
Circle::Circle(double R)
{
cout<<"Constructor"<<endl;
this->R = R;
}
Circle::Circle(Circle &A)
{
cout<<"Copy Constructor"<<endl;
this->R = A.R;
}
double Circle::area()
{
return PI*R*R;
}
double Circle::girth()
{
return 2*PI*R;
}
int main()
{
Circle A(5);
Circle B(A);
return 0;
}
定义了一个Circle圆形类,分别定义了带参数的构造函数和拷贝构造函数,然后在主函数中定义A对象,并传入初始值,调用带参数的构造函数。及定义B对象,通过A对象来初始化B对象。运行结果如下:
第一次定义的A对象调用带参数的构造函数,第二个B对象由于是通过A对象来初始化,所以调用拷贝构造函数。
所以,当类中有指针类型时,依靠默认的拷贝构造函数的方法,已经无法满足我们的需求,必须定义一个特定的拷贝构造函数,即不仅可以进行数据的拷贝,也可以为成员分配内存空间,实现真正的拷贝,也叫做深拷贝,这就是深拷贝构造函数。
6.C++浅拷贝与深拷贝实例详解
浅拷贝与深拷贝
在上一节讲解的拷贝构造函数的例子Circle类中,拷贝的策略都是与系统默认的策略一致,即把原有对象中成员依次拷贝给新对象中对应的成员,既然如此,我们为何还要自己定义呢?原因在于,简单的将所有情况都按照这种简单的方式初始化,难免有不同的情况,出现问题。
例如,刚才的Circle类中,如果成员变量中加一个指针成员,初始化中需要动态开辟内存,则会出现极大的安全隐患,代码如下:
#include<iostream>
#include<Cstring>
using namespace std;
#define PI 3.1415
class Circle
{
private:
double R;
char *str;
public:
Circle(double R,char *str);
~Circle();
double area();
double girth();
};
Circle::~Circle()
{
delete []str;
}
Circle::Circle(double R,char *str)
{
cout<<"Constructor"<<endl;
this->R = R;
this->str = new char[strlen(str)+1];
strcpy(this->str,str);
cout<<this->R<<" "<<this->str<<endl;
}
double Circle::area()
{
return PI*R*R;
}
double Circle::girth()
{
return 2*PI*R;
}
int main()
{
Circle A(5,"NO.1 Old class");
Circle B(A);
return 0;
}
为了验证,在Circle类中,我们增加了一个指针成员,并且在构造函数中对其初始化,同时没有自定义拷贝构造函数。那么在主函数中Circle B(A);的这句话将A对象赋值给B对象,将调用默认生成的拷贝构造函数,运行后,程序如下图报错:
而实际上的原因在于,默认的拷贝构造函数仅仅是进行数据赋值,并不能为指针开辟内存空间,相当于代码:
This->str = str;
那么本质上,也就是两个指针指向一块堆空间。已经违背了我们的初衷。那么在程序结束的时候,两个对象回收的时候,会调用自己的析构函数,释放这块内存空间,由于两个对象要调用两次,即delete两次,就会出现错误!
#include<iostream>
#include<Cstring>
using namespace std;
#define PI 3.1415
class Circle
{
private:
double R;
char *str;
public:
Circle(double R,char *str);
Circle(Circle &A);
~Circle();
double area();
double girth();
};
Circle::~Circle()
{
delete []str;
cout<<"Call Destructor"<<endl;
}
Circle::Circle(Circle &A)
{
cout<<"Copy Constructor"<<endl;
this->R = A.R;
this->str = new char[strlen(A.str)+1];
strcpy(this->str,A.str);
}
Circle::Circle(double R,char *str)
{
cout<<"Constructor"<<endl;
this->R = R;
this->str = new char[strlen(str)+1];
strcpy(this->str,str);
}
double Circle::area()
{
return PI*R*R;
}
double Circle::girth()
{
return 2*PI*R;
}
int main()
{
Circle A(5,"NO.1 Old class");
Circle B(A);
return 0;
}
7.C++中的this指针
this指针
一个类当中,有一个很隐蔽的特殊指针,它就是this指针!
为什么说它特殊?**因为只要定义一个类,系统就会预定义个名字叫做this名且指向当前对象的指针。**虽然我们看不到但却可以使用它。
比如,我们来看一个时钟类的一个成员函数,用来设置时间传值的代码:
int Clock::SetTime(int h,int m,int s)
{
H = h;
M = m;
S = s;
}
可以看到Clock类本身的成员变量为H、M、S,要用外部传来的值给它们三个赋值,为了区别它们,我们把形参定义成小写。那么如果我们知道了this的存在,就可以这样写:
int Clock::SetTime(int h,int m,int s)
{
this->H = h;
this->M = m;
this->S = s;
}
//也可以写成:
int Clock::SetTime(int h,int m,int s)
{
(*this).H = h;
(*this).M= m;
(*this).S= s;
}
上两种写法用到了对象中的隐藏的this指针,可以明确是本类中的成员,从而明显的区别本对象与外部变量。实际上,当一个对象调用其成员函数的时候,即便程序中有多个该类的对象,但成员函数的代码也仅有一份,所以为了区分它们是哪个对象调用的成员函数,编译器也是转化成this->成员函数这种形式来使用的。
8.C++友元函数的使用方法
知道类中的私有成员,只有被类里的成员函数访问,在类外是不能访问的。这体现了C++中类设计的封装、隐蔽思想,是C++最基本的优点。
但如果偶尔有的时候,我们在类外又确实想访问这些私有成员,就会变得麻烦很多,就处于既访问不到又不能声明为public类型的两难处境。
友元的出现就可以很好的解决这个问题,即把外部的函数声明为友元类型,赋予它可以访问类内私有成员的权利,来做到两全其美。这就是友元的意义,从字面意思也可以看出来,像“朋友”一样,开了一个绿色通道
友元的对象,它可以是全局的一般函数,也可以是其他类里的成员函数,这种叫做友元函数。不仅如此,友元还可以是一个类,这种叫做友元类。
理解友元的意义和作用后,我们来看怎么在C++中使用。对于友元函数,只需要在类内对这个函数进行声明,并在之前加上friend关键字。这个函数就具有了独特的权限,成为友元函数。
友元并不属于这个类本身,无论是友元函数还是友元类。都不能使用类内的this指针,同时也不可以被继承,如同父亲的朋友不一定是儿子的朋友这个道理。
如:求两点的距离
#include<iostream>
#include<math.h>
using namespace std;
class Point
{
private:
double x;
double y;
public:
Point(double a,double b)
{
x = a;
y = b;
}
int GetPoint()
{
cout<<"("<<x<<","<<y<<")";
return 0;
}
friend double Distance(Point &a,Point &b);
};
//求两点之间的距离
double Distance(Point &a,Point &b)
{
double xx;
double yy;
xx = a.x-b.x;
yy = a.y-b.y;
return sqrt(xx*xx+yy*yy);
}
int main()
{
Point A(2.0,3.0);
Point B(1.0,2.0);
double dis;
dis = Distance(A,B);
cout<<dis<<endl;
return 0;
}
9.C++友元类的使用方法
学习友元函数之后,我们再来看友元类,也是一样的道理和使用方法。如果把一个类A声明为另一个类B的友元类,则类A中的所有成员函数都可以访问B类中的成员,使用方法也一样,在类B中进行声明即可。
#include<iostream>
#include<math.h>
using namespace std;
class Point
{
private:
double x;
double y;
public:
Point(double a,double b)
{
x = a;
y = b;
}
int GetPoint()
{
cout<<"("<<x<<","<<y<<")";
return 0;
}
int distancetoLine()
{
}
friend class Tool;
};
class Tool
{
public:
double GetX(Point &A)
{
cout<<A.x<<endl;
return A.x;
}
double GetY(Point &A)
{
cout<<A.y<<endl;
return A.y;
}
double dis(Point &A)
{
cout<<sqrt(A.x*A.x+A.y*A.y)<<endl;
return sqrt(A.x*A.x+A.y*A.y);
}
};
int main()
{
Point A(2.0,3.0);
Tool T;
T.GetX(A);
T.GetY(A);
T.dis(A);
return 0;
}
可以看到我们又定义了一个工具类,可以获取一个点类的横、纵坐标以及求出这个点距离原点的距离,由三个方法实现,封装到一个类Tool里。并且在Point类里进行友元类的声明,可以看到使用方法。
优点:更方便快捷的访问类内的私有成员。
缺点:打破了C++中的封装思想。
10.C++中常数据的使用及初始化
常数据成员的使用及初始化
常的概念我们在学C语言时候就有了解,关键字是const,所谓的“常”,或者说被“常”修饰的变量,是不可以被改变的,比如用const修饰的一个变量就成了常变量,这个值不可被更改。
C++中,一样有常的概念,额外不同的是,const除了可以修饰一般的变量为常变量之外,还可用于修饰某个对象,变成常对象。以及可以修饰类的数据成员和成员函数,分别叫做类的常数据成员和常成员函数。
- 常数据成员:
对于常数据成员的用法,与我们在C语言的用法一样,只不过这部分数据出现在类里,使用的格式如下:
数据类型 const 数据成员名;
或const 数据类型 数据成员名;
被const修饰的成员则必须进行初始化,并且不能被更改,而初始化的方式则是在类的构造函数的初始化列表里进行的。
另外,有一个特殊情况,如果成员是static类型,即静态常数据成员,因为是静态的属性,初始化则需在类外进行初始化。下面我们把刚才讲的情况放到代码里来说明:
#include<iostream>
using namespace std;
class Clock
{
private:
const int h; //修饰h为常类型成员
const int m; //修饰m为常类型成员
int const s; //和上面两种用法都可以
static const int x;
public:
Clock(int a,int b,int c):h(a),m(b),s(c)
{
cout<<"Constrctor! Called"<<endl;
}
int ShowTime()
{
cout<<h<<":"<<m<<":"<<s<<endl;
return 0;
}
int GetX()
{
cout<<x<<endl;
return 0;
}
};
const int Clock::x = 99;
int main()
{
Clock A(12,10,30);
A.ShowTime();
A.GetX();
return 0;
}
注意看类中的四个常数据成员,其中X为static类型,因其静态化的特殊属性则需要在类外初始化,即便X为私有类型!
- 常对象:
C++中可以把一个对象声明为const类型,即常对象。这样声明之后,这个对象在整个生命周期中就不可以再被更改,所以在定义的时候要由构造函数进行初始化,定义格式如下:
类型 const 对象名;
或
const 类型 对象名;
需要注意的是,常对象不可以访问类中的非常成员函数,只能访问常成员函数(后面第三部分)。下面我们看例子:
#include<iostream>
using namespace std;
class Clock
{
private:
const int h; //修饰h为常类型成员
const int m; //修饰m为常类型成员
int const s; //和上面两种用法都可以
int x;
public:
Clock(int a,int b,int c):h(a),m(b),s(c)
{
x=99;
cout<<"Constrctor! Called"<<endl;
}
int ShowTime()
{
cout<<h<<":"<<m<<":"<<s<<endl;
return 0;
}
int GetX() const
{
//x=99;
cout<<x<<endl;
return 0;
}
};
int main()
{
const Clock A(12,10,30);
const Clock B(14,20,50);
//A = B;
//A.ShowTime();
A.GetX();
return 0;
}
注意看代码中语句,正常编译将报错,原因是A为常对象不可以被赋值,以及ShowTime函数为非常成员函数。
- 常成员函数:
类似的,一个类中的成员函数被const修饰后,就变成了常成员函数,常成员函数的定义如下:
返回类型 函数名(形参表列) const;
需要注意:
(1)常成员函数的定义和声明部分都要包含const;
(2)常成员函数只能调用常成员函数,而不能调用非常成员函数,访问但不可以更改非常成员变量。
#include<iostream>
using namespace std;
class Clock
{
private:
const int h; //修饰h为常类型成员
const int m; //修饰m为常类型成员
int const s; //和上面两种用法都可以
int x;
public:
Clock(int a,int b,int c):h(a),m(b),s(c)
{
x=99;
cout<<"Constrctor! Called"<<endl;
}
int ShowTime()
{
cout<<h<<":"<<m<<":"<<s<<endl;
return 0;
}
int GetX() const
{
//x=99;
cout<<x<<endl;
return 0;
}
};
int main()
{
const Clock A(12,10,30);
A.GetX();
return 0;
}
注意第29、30行代码,如果使用29行代码则会编译报错
总结
天才不受意志的支配,直面向美的欣赏,一切美所给予的欢悦,艺术所提供的安慰,是他完全忘却生活的烦恼。天才乐于孤独寂寞,一个人热衷于社交的程度恰正相当于他在理智上贫乏和庸俗的程度。
来自“https://www.dotcpp.com/course”总结