CPP类和对象
CPP类的定义和对象的创建
类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化。每个对象都是类的一个具体实例(Instance),拥有类的成员变量和成员函数。
类的定义
类是用户自定义的类型,如果程序中要用到类,必须提前说明,或者使用已存在的类(别人写好的类、标准库中的类等),C++语法本身并不提供现成的类的名称、结构和内容。
#include <iostream>
using namespace std;
class Student{
public:
char *name;
int age;
double score;
void say() {
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
};
int main(void) {
class Student stu;
stu.name = "小明";
stu.age = 18;
stu.score = 99.4;
stu.say();
return 0;
}
class是 C++ 中新增的关键字,专门用来定义类。Student是类的名称;类名的首字母一般大写,以和其他的标识符区分开。{ }内部是类所包含的成员变量和成员函数,它们统称为类的成员(Member);由{ }包围起来的部分有时也称为类体,和函数体的概念类似。public也是 C++ 的新增关键字,它只能用在类的定义中,表示类的成员变量或成员函数具有“公开”的访问权限。
类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。
创建对象
Student stu;
在创建对象时,class 关键字可要可不要,但是出于习惯我们通常会省略掉 class 关键字
访问类的成员
stu.name = "hello"
stu.say();
创建对象以后,可以使用点号.来访问成员变量和成员函数,这和通过结构体变量来访问它的成员类似
stu 是一个对象,占用内存空间,可以对它的成员变量赋值,也可以读取它的成员变量。
类通常定义在函数外面,当然也可以定义在函数内部,不过很少这样使用。
使用对象指针
上面代码中创建的对象 stu 在栈上分配内存,需要使用&获取它的地址,例如:
Student stu;
Student *pStu = &stu;
pStu 是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。
当然,你也可以在堆上创建对象,这个时候就需要使用前面讲到的new关键字,例如:
Student *pStu = new Student;
在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。但是通过 new 创建出来的对象就不一样了,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。
有了对象指针后,可以通过箭头->来访问对象的成员变量和成员函数,这和通过结构体指针来访问它的成员类似,请看下面的示例:
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
本次实例
#include <iostream>
using namespace std;
class Student {
public:
char *name;
int age;
double score;
void say() {
cout<<name<<"‘s age is "<<age<<", and score is "<<score<<endl;
}
};
int main (void) {
Student *pStu = new Student;
pStu->name = "wangjun";
pStu->age = 16;
pStu->score = 99.0;
pStu->say();
return 0;
}
总结
本节重点讲解了两种创建对象的方式:一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上创建,必须要用一个指针指向它,读者要记得 delete 掉不再使用的对象。
通过对象名字访问成员使用点号.,通过对象指针访问成员使用箭头->,这和结构体非常类似。
CPP类的成员变量和成员函数
类可以看做是一种数据类型,它类似于普通的数据类型,但又有别于普通的数据类型,类这种数据类型是包含成员变量和成员函数的集合
类的成员变量和普通变量一样,也有数据类型和名称,占用固定长度的内存。但是在定义类的时候不能对成员变量赋值,因为类只是一种数据类型,或者说是一种模板,本身不占用内存空间,而变量的值则需要内存来存储。
类的成员函数也和普通函数一样,都有返回值和参数列表,它与一般函数的区别是:成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;而普通函数是独立的,作用范围是全局的,或位于某个命名空间内。
#include <iostream>
#include <iomanip>
using namespace std;
class Student {
public:
char *name;
int age;
double score;
void say() {
cout<<name<<" 的年龄是 "<<age<<", 成绩是 "<<setprecision(8)<<score<<endl;
}
};
int main (void) {
Student *pStu = new Student;
pStu->name = "wangjun";
pStu->age = 16;
pStu->score = 99.5;
pStu->say();
return 0;
}
这段代码在类体中定义了成员函数。你也可以只在类体中声明函数,而将函数定义放在类体外面,如下:
#include <iostream>
#include <iomanip>
using namespace std;
class Student {
public:
char *name;
int age;
double score;
void say();
};
void Student::say() {
cout<<name<<" 的年龄是 "<<age<<",成绩是 "<<score<<endl;
}
int main (void) {
Student *pStu = new Student;
pStu->name = "wangjun";
pStu->age = 16;
pStu->score = 99.5;
pStu->say();
return 0;
}
在类体中直接定义函数时,不需要在函数名前面加上类名,因为函数属于哪一个类是不言而喻的。
但当成员函数定义在类外时,就必须在函数名前面加上类名予以限定。::被称为域解析符(也称作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类。
成员函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前。
inline成员函数
在类体中和类体外定义成员函数是有区别的:在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。当然,在类体内部定义的函数也可以加 inline 关键字,但这是多余的,因为类体内部定义的函数默认就是内联函数。
内联函数一般不是我们所期望的,它会将函数调用处用函数体替代,所以我建议在类体内部对成员函数作声明,而在类体外部进行定义,这是一种良好的编程习惯,实际开发中大家也是这样做的。
当然,如果你的函数比较短小,希望定义为内联函数,那也没有什么不妥的。
如果你既希望将函数定义在类体外部,又希望它是内联函数,那么可以在定义函数时加 inline 关键字。当然你也可以在函数声明处加 inline,不过这样做没有效果,编译器会忽略函数声明处的 inline。
CPP类成员的访问权限
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。
理解1:在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
理解2:在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
下面通过一个 Student 类来演示成员的访问权限:
#include <iostream>
using namespace std;
class Student {
private:
char *mName;
int mAge;
double mScore;
public:
void setname(char *name);
void setage(int age);
void setscore(double score);
void show();
};
void Student::setname(char *name) {
mName = name;
}
void Student::setage(int age) {
mAge = age;
}
void Student::setscore(double score) {
mScore = score;
}
void Student::show() {
cout<<mName<<"‘s age is "<<mAge<<", score is "<<mScore<<endl;
}
int main(void) {
Student stu1;
stu1.setname("Jim");
stu1.setage(19);
stu1.setscore(99.1);
stu1.show();
Student *stu2 = new Student;
stu2->setname("Tome");
stu2->setage(22);
stu2->setscore(98.7);
stu2->show();
return 0;
}
类的声明和成员函数的定义都是类定义的一部分,在实际开发中,我们通常将类的声明放在头文件中,而将成员函数的定义放在源文件中。
#include <iostream>
using namespace std;
class Student {
private:
char *m_name;
int m_age;
double m_score;
public:
void setname(char *name);
void setage(int age);
void setscore(double score);
void show();
};
void Student::setname(char *name) {
m_name = name;
}
void Student::setage(int age) {
m_age = age;
}
void Student::setscore(double score) {
m_score = score;
}
void Student::show() {
cout<<m_name<<"‘s age is "<<m_age<<", score is "<<m_score<<endl;
}
int main(void) {
Student stu1;
stu1.setname("Jim");
stu1.setage(14);
stu1.setscore(98.7);
stu1.show();
Student *stu2 = new Student;
stu2->setname("Tom");
stu2->setage(19);
stu2->setscore(99.5);
stu2->show();
return 0;
}
类中的成员变量 m_name、m_age 和m_ score 被设置成 private 属性,在类的外部不能通过对象访问。也就是说,私有成员变量和成员函数只能在类内部使用,在类外都是无效的。
成员函数 setname()、setage() 和 setscore() 被设置为 public 属性,是公有的,可以通过对象访问。
private 后面的成员都是私有的,直到有 public 出现才会变成共有的;public 之后再无其他限定符,所以 public 后面的成员都是共有的。
成员变量大都以m_开头,这是约定成俗的写法,不是语法规定的内容。以m_开头既可以一眼看出这是成员变量,又可以和成员函数中的形参名字区分开。
以 setname() 为例,如果将成员变量m_name的名字修改为name,那么 setname() 的形参就不能再叫name了,得换成诸如name1、_name这样没有明显含义的名字,否则name=name;这样的语句就是给形参name赋值,而不是给成员变量name赋值。
因为三个成员变量都是私有的,不能通过对象直接访问,所以必须借助三个 public 属性的成员函数来修改它们的值。下面的代码是错误的:
Student stu;
//m_name、m_age、m_score 是私有成员变量,不能在类外部通过对象访问
stu.m_name = "小明";
stu.m_age = 15;
stu.m_score = 92.5f;
stu.show();
简单地谈类的封装
private 关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为 public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为 private。
根据C++软件设计规范,实际项目开发中的成员变量以及只在类内部使用的成员函数(只被成员函数调用的成员函数)都建议声明为 private,而只将允许通过对象调用的成员函数声明为 public。
另外还有一个关键字 protected,声明为 protected 的成员在类外也不能通过对象访问,但是在它的派生类内部可以访问,这点我们将在后续章节中介绍,现在你只需要知道 protected 属性的成员在类外无法访问即可。
有读者可能会提出疑问,将成员变量都声明为 private,如何给它们赋值呢,又如何读取它们的值呢?
我们可以额外添加两个 public 属性的成员函数,一个用来设置成员变量的值,一个用来修改成员变量的值。上面的代码中,setname()、setage()、setscore() 函数就用来设置成员变量的值;如果希望获取成员变量的值,可以再添加三个函数 getname()、getage()、getscore()。
给成员变量赋值的函数通常称为 set 函数,它的名字通常以set开头,后跟成员变量的名字;读取成员变量的值的函数通常称为 get 函数,它的名字通常以get开头,后跟成员变量的名字。
除了 set 函数和 get 函数,在创建对象时还可以调用构造函数来初始化各个成员变量,我们将在《C++构造函数》一节中展开讨论。不过构造函数只能给成员变量赋值一次,以后再修改还得借助 set 函数。
这种将成员变量声明为 private、将部分成员函数声明为 public 的做法体现了类的封装性。
所谓封装,是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。
有读者可能会说,额外添加 set 函数和 get 函数多麻烦,直接将成员变量设置为 public 多省事!确实,这样做 99.9% 的情况下都不是一种错误,我也不认为这样做有什么不妥;但是,将成员变量设置为 private 是一种软件设计规范,尤其是在大中型项目中,还是请大家尽量遵守这一原则。
为了减少代码量,方便说明问题,本教程中的类可能会将成员变量设置为 public,请读者不要认为这是一种错误。
对private和public的更多说明
声明为 private 的成员和声明为 public 的成员的次序任意,既可以先出现 private 部分,也可以先出现 public 部分。如果既不写 private 也不写 public,就默认为 private。
在一个类体中,private 和 public 可以分别出现多次。每个部分的有效范围到出现另一个访问限定符或类体结束时(最后一个右花括号)为止。但是为了使程序清晰,应该养成这样的习惯,使每一种成员访问限定符在类定义体中只出现一次。
C++类对象的内存模型
类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。
直观的认识是,如果创建了 10 个对象,就要分别为这 10 个对象的成员变量和成员函数分配内存,如下图所示:
不同对象的成员变量的值可能不同,需要单独分配内存来存储。但是不同对象的成员函数的代码是一样的,上面的内存模型保存了 10 分相同的代码片段,浪费了不少空间,可以将这些代码片段压缩成一份。
事实上编译器也是这样做的,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。如下图所示:
成员变量在堆区或栈区分配内存,成员函数在代码区分配内存。
32位系统,vc编译器中,
short占 2 字节,
int 、float、long 都占 4 字节,
只有double 占8 字节
(容易弄错的就是 short 和 long)
#include <iostream> //C++标准输入输出库
using namespace std; //C++标准命名空间
class Student { //类的定义
private: //私有成员类型
char *m_name; //成员名用m_ 以和之后函数形参相区别
int m_age;
double m_score;
public: //公有成员类型
void setname(char *name); //成员函数 返回值 函数名 形参列表
void setage(int age);
void setscore(double score);
void show();
};
void Student::setname(char *name) { //类外对函数的定义——要用::域解析符表明函数属于哪个类
m_name = name; //成员函数的定义也属于类定义的一部分,所以可以使用类成员变量,
} // 利用成员函数中的形参对成员变量进行赋值,再把函数作为接口提供给外界使用
void Student::setage(int age) {
m_age = age;
}
void Student::setscore(double score) {
m_score = score;
}
void Student::show() {
cout<<m_name<<"‘s age is "<<m_age<<", score is "<<m_score<<endl;
}
int main(void) {
Student stu1; //在栈上创建对象,操作类成员变量和成员函数用‘.’符号
stu1.setname("Jim");
stu1.setage(14);
stu1.setscore(98.7);
stu1.show();
Student *stu2 = new Student; //在堆上创建对象,new 与 delete 成对出现
stu2->setname("Tom"); //操作类成员用->符号
stu2->setage(19);
stu2->setscore(99.5);
stu2->show();
cout<<sizeof(stu1)<<endl; //类可以看做是一种复杂的数据类型,也可以用sizeof求得该类型的大小。
cout<<sizeof(stu2)<<endl;
cout<<sizeof(*stu2)<<endl;
cout<<sizeof(Student)<<endl;
delete stu2;
return 0;
}
Student 类包含三个成员变量,它们的类型分别是 char *、int、float,都占用 4 个字节的内存,加起来共占用 12 个字节的内存。通过 sizeof 求得的结果等于 12,恰好说明对象所占用的内存仅仅包含了成员变量。
类可以看做是一种复杂的数据类型,也可以使用 sizeof 求得该类型的大小。从运行结果可以看出,在计算类这种类型的大小时,只计算了成员变量的大小,并没有把成员函数也包含在内。
对象的大小只受成员变量的影响,和成员函数没有关系。
假设 stu 的起始地址为 0X1000,那么该对象的内存分布如下图所示:
m_name、m_age、m_score 按照声明的顺序依次排列,和结构体非常类似,也会有内存对齐的问题。