一、类和引用
1--传参和返回值
使用类作为函数参数时,通常采用引用的方法。可以把类的引用作为形参,也可以把类的引用作为返回值。具体方法如下,这是一个函数声明,假设Student是一个类:
Student& function_name(Student param);
前面的Student& 说明函数的返回类型是关于Student类的引用,后面的Student param说明传入了一个Student类型的对象作为实参,形参是param!
2--返回值为引用的优势
如果返回值为自身的引用,可以简化代码,实现多个函数连续使用的效果,如下,若student1是一个Student类型的对象,可以实现这样的效果:
student1.operate1().operate2();
这和下面的代码等效:
student1.operate1();
student1.operate2();
为此,需要这样设计类和类函数:
#include <iostream>
using std::cout;
using std::endl;
class Student
{
private:
int age;
int grade;
public:
Student():age(10),grade(1)
{
cout<<"age:"<<age<<endl;
cout<<"grade:"<<grade<<endl;
}
Student& add_age(int num)
{
age+=num;
cout<<"age:"<<age<<endl;
cout<<"grade:"<<grade<<endl;
return *this;
}
Student& minus_age(int num)
{
age-=num;
cout<<"age:"<<age<<endl;
cout<<"grade:"<<grade<<endl;
return *this;
}
};
int main(void)
{
Student student1;
student1.add_age(1).minus_age(2);
}
上面的代码中示范了如何设计类和类方法达到简化的效果,需要注意的是:
如果需要返回对象自身的引用,需要使用this指针的解引用*this(注意,如果我需要返回对象a的引用,我不需要也不能刻意地去强调&a,这样就会返回对a地址地引用了。因为在函数原型已经定义了返回值是一个引用,编译器会自动识别这一点)
类似student1.add_age(1).minus_age(2)的代码是从左到右执行的。读者可以自行复制代码进行验证。
读者可能觉得,这种方法好像没有什么用。但由于许多常用的cpp库函数都是使用了这种方法,所以我觉得有必要进行特意的说明!
比方说,我们喜闻乐见的cin.get(string,len)方法,读者可以查阅本系列的第三章。
3--三个const
笔者在阅读《c++ primer plus》这本书时,诧异地发现了某个类方法的声明和定义一口气用了三个const!!!
const Student& function_name(const Student& student2) const;
const Student& function_name(const Student& student2) const
{
......
}
下面我来分别进行解释
第一个const:强调返回类型为const限定的引用
意味着返回后不能再改变这个对象。我们对上面一段代码进行调整,把add_age这个类方法的返回类型改为const类型的引用。然后,毫不意外,它报错了!
#include <iostream>
using std::cout;
using std::endl;
class Student
{
private:
int age;
int grade;
public:
Student():age(10),grade(1)
{
cout<<"age:"<<age<<endl;
cout<<"grade:"<<grade<<endl;
}
const Student& add_age(int num)
{
age+=num;
cout<<"age:"<<age<<endl;
cout<<"grade:"<<grade<<endl;
return *this;
}
Student& minus_age(int num)
{
age-=num;
cout<<"age:"<<age<<endl;
cout<<"grade:"<<grade<<endl;
return *this;
}
};
int main(void)
{
Student student1;
student1.add_age(1);//正确
student1.add_age(1).minus_age(2);//报错
}
第二个const:强调不能通过显式的方法通过引用改变特定对象的值,在这里,这个特定对象是student2。
第三个const:强调不能通过隐式的方法改变特定对象的值,在这里,这个特定对象是this指针指向的对象。
读者可能对何为显式访问,何为隐式访问感到疑惑,不要紧,我现在对其进行具体的说明,先看看下面的例子:
#include <iostream>
using std::cout;
using std::endl;
class Student
{
private:
int age;
int grade;
public:
Student(int p_age,int p_grade)
{
age=p_age;
grade=p_grade;
}
int compare_age(Student& student2)
{
if (student2.age<age)
return 1;
else if (student2.age==age)
return 0;
else
return -1;
}
};
int main(void)
{
Student student1=Student(20,3);
Student student2(19,2);
cout<<student1.compare_age(student2);
}
我们看其中的一个具体的比较函数compare_age:
int compare_age(Student& student2)
{
if (student2.age<age)
return 1;
else if (student2.age==age)
return 0;
else
return -1;
}
这个函数要实现的功能是比较两个学生的年龄,那就必须要去访问访问这两个学生(对象)。
student2是通过引用参数访问的,所以被称为“显式”的访问;student1事实上是通过我们在第七章所述的this指针访问的,所以被称为“隐式”的访问。也就是说,使用方法的对象,实质上是通过隐式的方法访问的。
那么,const限定的是哪一种访问方式,就很好理解了!
二、对象数组
和结构体和结构体数组一样,类也支持对象数组的存在!下面给出具体的介绍:
1--定义方法
和int类型数组的定义方法一模一样:
Student name_of_array[NUM_OF_ARRAY];
这里,Student是类的名称,name_of_array是对象数组名称,NUM_OF_ARRAY是定义的元素数量。
2--对象数组名称可以被隐式转化吗?
我们知道int型数组可以被隐式转化为指向其第一个元素的指针,就像这样:
#include <iostream>
using namespace std;
int main(void)
{
int a[10];
*a=10;
cout<<a[0];
}
输出结果毫不意外地是10,但对象数组也能这样吗?这个在《c++ primer plus》上面也没有,不过笔者经过实验,发现结果是肯定的!C的这种优良性质还是被C++继承了!对象数组的名称确实能隐式转化为指向对象数组第一个元素的指针!
#include <iostream>
class Student
{
int age;
public:
void age_change(int num)
{
age=num;
}
void read_age()
{
std::cout<<age;
}
};
int main(void)
{
Student student_x[10];
(*student_x).age_change(5);
student_x[0].read_age();
}
上述程序,结果输出为5,符合我们的预期!
3--对象数组和构造函数
如果对象数组对应的类的构造函数需要传递参数,我们就要一个一个地为对象数组地元素调用构造函数,形式如下:
假设构造函数为
Student::Student(int num)
{
age=num;
}
那么定义数组时应该这么做:
Student student_x[4]=
{
Student(1),
Student(2),
Student(3),
Student(4)
}
当然,把等号去掉也是行的,这符合C++声明数组的规则
Student student_x[4]
{
Student(1),
Student(2),
Student(3),
Student(4)
};
(这里说的C++声明数组的规则,是int a[3]={1,2,3}和int a[3]{1,2,3}等价,这是C++11的性质)
如果认为定义了一个不用传参的构造函数,或者使用默认构造函数,则不需要像上面一样复杂的初始化!
比方说,下面这一段代码,输出结果是四个hello,说明构造函数被隐式调用了四次!
#include <iostream>
class Student
{
int age;
public:
Student()
{
std::cout<<"hello"<<std::endl;
}
void age_change(int num)
{
age=num;
}
void read_age()
{
std::cout<<age;
}
};
int main(void)
{
Student student_x[4];
}
又比方说,下面这段代码也是可以运行的!
#include <iostream>
class Student
{
int age;
public:
Student():age(0)
{
std::cout<<"hello"<<std::endl;
}
void age_change(int num)
{
age=num;
}
void read_age()
{
std::cout<<age;
}
};
int main(void)
{
Student student_x[4];
}
如果构造函数进行了重载操作,则可以在一个对象数组的初始化中混合使用不同的构造函数,示例如下:
#include <iostream>
class Student
{
int age;
public:
Student():age(0)
{
std::cout<<"hello"<<std::endl;
}
Student(int num)
{
age=num;
}
};
int main(void)
{
Student student_x[4]
{
Student(),
Student(2),
Student(),
Student(4)
};
}
如果有多个构造函数且有一个构造函数不用传递参数,那么初始化时可以缺省构造函数。缺省部分会被无参数构造函数补充:
#include <iostream>
class Student
{
int age;
public:
Student():age(0)
{
std::cout<<"hello"<<std::endl;
}
Student(int num)
{
age=num;
}
};
int main(void)
{
Student student_x[4]
{
Student(),
Student(2)
};
}
以上代码,声明了一个有4个元素的对象数组,但却只显式调用了两个构造函数。这说明student_x[0]和student_x[1]分别调用了无参数构造函数和有参数构造函数,而后面两个对象则隐式调用了无参数构造函数!
4--访问对象数组
有两种方式访问对象数组
第一种-常规方法:
student_x[2].func()
把student_x[2]当作一个独立的对象即可
第二种-指针运算:
既然我们已经明确对象数组也可以如同普通数组一样可以隐式转化为一个指针,那么下面的访问方式无疑是可行的:
*(student_x+2).func()
当然,这等效于:
(student_x+2)->func
具体实例如下:
#include <iostream>
class Student
{
int age;
public:
Student():age(0)
{
}
Student(int num)
{
age=num;
}
void add_age(int num)
{
age+=num;
}
void read_age()
{
std::cout<<age;
}
};
int main(void)
{
Student student_x[4]
{
Student(),
Student(2),
Student(3),
Student(2)
};
(student_x+2)->add_age(2);
(student_x+2)->read_age();
}
读者可以自行编译验证其正确性!
三、类作用域
类在事实上提供了一种新的作用域,这种作用域不同于全局和局部!
类对象和类成员函数(类方法)的作用域都为类,这意味着,在类声明和成员函数定义中,可以直接使用未用作用域限限定符::修饰的变量。在其他情况下,要使用类成员名时,必须根据上下文使用直接成员运算符(.)、间接成员运算符(->)或作用域解析运算符(::)。