这一讲初步讲解关于类的东西,我们先来看一个日期类, Date 的设计,Date 有一个有年月日成员,然后有一些访问成员函数,看不明白不要紧,我们慢慢来理解下面这段代码就行了,这一节就是理解这段代码中的语法
类的相关语法
#include <iostream>
using namespace std;
class Date{
int yy;//year
int mm;//month
int dd;//date
static int cnt;//统计有多少个日期类被创建了
public:
//构造函数
Date(int y,int m,int d){
yy = y;
mm = m;
dd = d;
cnt++;
}
~Date(){
return;
}
int year()const{
return yy;
}
int month()const{
return mm;
}
void set_year(int y){// 加const就会报错
yy = y;
}
int date()const;
int num_of_date()const{
return cnt;
}
};
int Date::cnt = 0;// 静态成员必须在某个地方重新定义
int Date::date() const{
return dd;
}
int main(){
Date d1(1,2,3);
cout << d1.year() <<" \n";
Date d2(2,3,4);
cout << d2.month() << " \n";
// 同样的cnt值
cout << d2.num_of_date() << " \n";
cout << d1.num_of_date() << "\n";
}
类的声明
声明方式很简单
class <类的名字>{
};// 注意这个分好不能丢掉
对照date类的声明
类的成员变量和成员函数
上面定义的一堆变量 yy/dd/mm
以及 cnt
这就是成员变量,而下面的那些函数(e.g. : year()),就是成员函数,所谓 类 就是一堆数据(成员变量)以及与之相关的操作(成员函数) 的集合
访问控制
现在我们来理解那个 public
关键字, 这个是什么呢,就是一个修饰,它表明有他修饰的(无论是变量还是函数) 你都可以在外边访问,反之没有他修饰的(class中默认为 private
),你都不能在外边访问,这里的date 类中 成员 yy/dd/mm/cnt
都是属于私有变量不能在外边访问的。比如如果你加上这一句
cout << d1.yy <<"\n" //编译器报错declared private here
编译器会提醒你,他被声明为私有的了。
总结两点:
- class 中默认所有成员(变量和函数) 均为 private
- 只有公有
public
才能在类外边访问
为什么要这么设计呢?为什么不全都作为公有呢?
这就是 封装 ,就是对于所设计的类,仅仅对外提供一些必要的接口, 而不是将数据全都暴露在外边。这样有两个好处:
- 对于外边的使用者来说使用更简单,只使用必要的接口
- 对于类的设计者更安全,让里面的数据不那么容易被使用者更改
就像一个人只把必要的东西露在外边,其他的都不给别人使用。
成员函数
我们看date类中的
int year()const{
return yy;
}
int month()const{
return mm;
}
void set_year(int y){// 加const就会报错
yy = y;
}
int date()const;
这些都是成员函数,当然最前面那个 Date
也是,那个是构造函数,我们等会儿再讲
和普通函数没啥区别,你可能注意到有的函数 (e.g.: set_year) 是定义在里面的,而有的(e.g.: date
) 是定义在外边的。这两者的区别你完全不用管,我推荐的方式是直接在类里面定义,当然也可以定义在外边,定义在外边的方式和普通函数的定义完全一样,唯一的区别就是在成员名字前面加 类名::
(告诉编译器这个名字来自哪里,后面的cnt的初始化方式也一样) 如date的定义 line-40
int Date::date() const{
return dd;
}
常量成员函数
相信你注意到了函数
int year()const
后面的关键字 const
有这种标识的就是常量成员函数,他的意思是函数里面不会对类的成员变量做更改,所有函数里面涉及修改的都不能声明为const (e.g. : set_year 声明为const 就会报错),
所以我们有这样一个设计原则
所有不涉及修改成员变量的都应该声明为const
静态成员变量与静态成员函数
你应该注意到这里有个 static int cnt
的声明,这是说 cnt 这个变量是静态的, 什么是static呢?就是说这个类的所有对象(main 函数里的 d1 和d2) 均只有一份,也就说所有对象都共有这个成员所以 调用 上面那个例子中调用 num_of_date
d1 d2 输出一样的结果
// class date
int main(){
Date d1(1,2,3);
cout << d1.year() <<" \n";
Date d2(2,3,4);
cout << d2.month() << " \n";
// 同样的cnt值
cout << d2.num_of_date() << " \n";
cout << d1.num_of_date() << "\n";
}
remark 不过需要注意的是,静态变量一定要在外边初始化,看开头的代码.
同理静态成员函数就是生命为 Static 的函数这种函数一般是与成员变量无关的保证该类任何对象调用都是一样的结果。
构造函数
构造函数就是对成员变量的初始化方法,可以定义多个,这里:
//构造函数
Date(int y,int m,int d){
yy = y;
mm = m;
dd = d;
cnt++;
}
可以看到构造函数唯一区别是没有返回值并且名字跟类名一样。当然也可以这样初始化,意思是一样的
Date(int y,int m,int d):yy(y),dd(d),mm(m){};
默认构造函数
所谓默认构造函数,首先他还是一个构造函数,默认构造函数就是没有参数的构造函数,或者构造函数的所有参数都有默认值的函数,作为声明类对象的默认构造函数,如果没有默认构造函数,声明的时候就会报错. 比如下面一个声明date类的例子
Date date;//error: no matching function for call to 'Date::Date()'
Date d[31];//error: no matching function for call to 'Date::Date()'
看他的报错 error: no matching function for call to 'Date::Date()'
也就是说默认他的初始化是用 没有参数的构造函数。所以没有参数的构造函数就是默认构造函数。我们来试着定义一个默认的静态日期default_date
作为默认构造函数的日期。
class{
....
static int cnt;//统计有多少个日期类被创建了
static Date default_date;
public:
....
Date(){
yy = default_date.yy;
mm = default_date.mm;
dd = default_date.dd;
}
};
默认构造的声明是没问题了,可是静态变量的初始化又会出问题了,他会报错 teE]+0x0): undefined reference to `Date::default_date'
所以这里 default_date 得像cnt一样在类外边定义一下,这里在类 Date外边定义
collect2.exe: error: ld returned 1 exit status
class Date{
...
};
Date Date::default_date(2018,6,5);
析构函数
Date 中 ~Date
就是析构函数,析构函数的声明就是
~类名(){
...
}
析构函数的作用是在,对象不被使用的时候,释放掉使用的内存,就是 new
(类似于 malloc) 出来的空间,在这个例子中没有用到,不过考虑另外一个例子,我们写一个和 STL 里面的 vector 类似的类Vec, 里面有一个成元变量是数组
他能够实现的操作是能像数组一样访问每个元素的值,并且能够动态的增长,注意看push_back() 函数
class Vec{
int *ele;
int capacity;// 数组ele的大小
int sz;// 添加的元素个数
public:
Vec(){
capacity = 8;
sz =0;
ele = new int[capacity];
}
Vec(int cap){
capacity = cap;
sz =0;
ele = new int[capacity];
}
void push_back(int val) {// 添加一个元素到数组末尾,如果已满则扩充空间
if(sz == capacity){//满了,重新分配空间并复制
capacity <<=1;
int * tmp = new int[capacity];
for(int i=0 ; i<sz ; ++i)tmp[i] = ele[i];
delete[]ele;
ele = tmp;
}
ele[sz++] = val;//末尾添加元素
}
int get_val(int idx)const {
return ele[idx];
}
int size()const{return sz;}
~Vec(){
delete[] ele;
}
};
因为每个对象都有一个数组成员占用很大一块空间(堆上分配), 所以得delete掉。这就是析构函数的作用,还有一种就是类里面有成员是另外一个类的时候你也应该对另外一个类析构。比如Vec类里面有个 Date 类的对象,你也应该析构掉 date,例如
class Vec{
int *p;
Date *d;
public:
Vec(){
p = new int[8];
}
~Vec(){
delete[] p;
delete []d;
}
};
复制构造函数(copy construct function)
考虑这条语句
Vec v1;
for(int i=0 ; i<3 ; ++i)v1.push_back(i);
//v1 = {0,1,2}
for(int i=0 ; i < v1.size() ; ++i)std::cout << v1.get_val(i) << '\n';
Vec v2 = v1;// 现在v2 里面的所有成员都和v1 相等
std::cout << "----------v2----------" << '\n';
for(int i=0 ; i<v2.size() ; ++i)std::cout << v2.get_val(i) << '\n';
语句 v2 = v1
这里就是默认使用了复制构造函数将v1的所有成员复制给了 v2,类似于构造函数
Vec(const Vec& v){
capacity = v.capacity;
sz = v.sz;
ele = v.ele;
}
但是这就引发了一个问题,对于有指针域的成语仅仅是简单的 复制指针的值而没有重新创建一块空间,导致 v1.ele 和 v2.ele 指向同一块内存,比如下面这条语句(接着上面的)
v1.push_back(5);
std::cout << v2.get_val(3) << '\n';// 注意这里v2的size仅仅为3,是不能访问到 v2.ele[3]的
会输出5,这几句说明他两的ele指向同一个地方了
所以这里就引出了另外一个知识,浅copy与深copy,刚才那种就是浅copy, 这里涉及指针域我们应该使用深copy表明他们不是同一个内存(不同对象怎么能用同一个内存呢?)
深copy
这里可以如下设置复制构造函数
Vec(const Vec& v){
capacity = v.capacity;
sz = v.sz;
ele = new int[capacity];
for(int i=0 ; i<sz ; ++i)ele[i] = v.get_val(i);
}
在运行刚才那条语句就不是 5 了而是其他的值,其实这里你将两个对象 v1/v2
的 ele
输出来就知道一不一样了。
总结一下:
- 默认复制构造函数是浅copy,只copy 成员域
- 涉及指针的成员应该使用深copy 使得不表示同一块内存
struct 与Class 的区别
在c++里 struct 与class 是一样的,只有访问控制不一样,struct 默认所有成员都是 public,而class默认所有成员都是 private。