类定义成员函数的方式于普通函数差不多,成员函数的声明必须在类内部,它的定义则既可以在类的内部,也可以在外部。
看一个成员函数的定义:
struct Sale_
{
string bookNo;
string isbn() const
{
return bookNo;
}
}
我们可以这样使用:Sale_ a_s;
a_s.bookNo=”123”;
a_s.isbn();//返回a_s的bookNo
他其实是隐式地返回a_s.bookNo的。成员函数isbn通过一个this的额外隐式参数来访问它的那个对象。
换而言之,成员函数的定义其实应该是这样的:
string isbn() const
{
return this->bookNo;
}
我们还应该看到,上面的isbn成员函数,在形参列表后面跟了个const,接下来我们解释一下它的用处。默认情况下,this的类型是Sale_ * const,就是说我们不能把它绑定在一个常量对象上,再说透一点,就是当对象是一个常量时,我们并不能调用该成员函数(否则就通过指针改变了一个常量啦)。如果我们在形参列表后面加上一个const,则表明this指针是一个指向常量的指针(常量对象都可以调用此函数了)。所以说,当成员函数不会改变对象的值的时候,形参后加个const会比较保险。
一、构造函数
每个类都分别定义了它的对象被初始化的方式,类会通过一个或几个特殊的成员函数来控制其对象的初始化工程,这些函数就称构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
当类没有声明任何构造函数时,编译器会自动生成默认构造函数。
下面是一个类
struct Sales_dara{
Sales_data() = default;
Sales_data(const std::string &s):bookNo(s){}
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(std::istream &);
std::string bookNo;
unsigned units_sold =0;
double revenue =0.0;
}
分析上面的代码,Sales_data() = default;
是一个默认构造函数,因为我们既需要其他形式的构造函数,也需要默认构造函数。(只有为内置类型的数据成员提供了初始值并且编译器支持类内初始值,此默认构造函数才有用)Sales_data(const std::string &s):bookNo(s){}
这行代码出现了冒号以及冒号和花括号之间的新内容,这部分内容称构造函数初始化列表,它负责为新创建的对象的一个或几个数据成员赋初值。不同成员的初始化通过逗号隔开。Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n),revenue(p*n) {}
可以看到上面两个构造函数中函数体都是空的。这是因为这些构造函数的唯一目的就是为数据成员赋初值,如果没有其他任务要执行,函数体就是为空的。
二、访问控制与封装
定义在public说明符之后的成员在整个程序都能被访问,public成员定义类的接口。
定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,也就是private部分封装了类的实现细节。
class与struct
这两个关键字都可以定义类,唯一的区别就在于,他们的默认访问权限不一样。
若使用struct关键字,则定义在第一个访问说明符之前的成员都是public的。
若使用class关键字,则定义在第一个访问说明符之前的成员都是private的。
封装的两个优点:
1、确保用户代码不会无意间破坏封装对象的状态。
2、被封装的类的具体实现可以随时改变,而无需调整用户级别的代码
三、友元
对于非成员函数或者是其它类,但是又想访问到一个类的私有的成员,则可以通过令其它类或者这个非成员函数成为该类的友元。如果是对于函数,只需要在该类里增加一条以friend关键字开始的函数声明语句即可(并不代表这个函数在外面就不用再声明了);如果是对于类,也是在需要被访问到私有成员的类中,增加一条以friend关键字开始的类声明。
如果存着两个类:A和B,此时A的clear函数想要访问B的私有成员,类之间定义的顺序是如何的呢?
- A先定义,但是clear函数只有声明(因为此时还无法访问B的私有成员)
- B再定义,定义时对A的clear函数进行友元声明(以便后续该函数可以访问B的私有成员)
- 在将clear函数进行类外定义
友元声明不是声明!
其他类和非成员函数想要成为友元时,他们真正的声明是不需要在友元声明之前的,但是,任何对他们的使用都必须要先进行真正的声明。
struct X{
friend void outerFunc() { cout << "hello world" << endl; }// 该函数在进行友元声明的同时,进行了定义,但却没有真正声明
X() { outerFunc(); }// 这是非法的,因为outerFunc没被声明
void g();
void h();
};
void X::g() { outerFunc(); }// 非法,因为outerFunc没被声明
void outerFunc();// 首次出现了对outerFunc的声明
void X::h() { outerFunc(); }// 正确
四、可变数据成员
有时,我们希望能修改类的某个数据成员,即使是在一个const成员函数内,这种情况下我们可以在变量的声明中加入mutable关键字。如下:
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const
{
++access_ctr;// 保存一个计数值,用于记录成员函数被调用的次数。
//…在进行一些其它的操作
}
如上所示,尽管some_member是一个const成员函数,他仍然能够改变access_ctr的值。因为该成员是个可变成员,在任何成员函数内(包括const函数在内)都可以改变该成员的值。
五、委托构造函数
委托构造函数就是使用它所属类的其它构造函数执行它自己的初始化。如下:
class Sales_data
{
public:
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s),units_sold(cnt),revenue(cnt *price){}//正常的构造函数
Sales_data():Sales_data(“”,0,0){} //委托构造函数
Sales_data(std::string s): Sales_data(s,0,0){} //委托构造函数
std::string bookNo;
unsigned units_sold =0;
double revenue =0.0;
}
六、隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制(但编译器只会自动地执行一步类型转换)
struct myStructTest{
int a = 0;
string b = "g";
myStructTest & combine(const myStructTest &other){
this->a += other.a;
this->b += other.b;
return *this;
}
myStructTest(int x) :a(x){ }// 存在整型到myStructTest对象的转换
myStructTest(string x) :b(x){ }// 存在string型到myStructTest对象的转换
myStructTest(int x, string y) :a(x), b(y){ }// 一个以上的参数就不会有转换
myStructTest() = default;
};
myStructTest ta;//默认构造了ta
ta.combine(5);//相当于先转换成myStructTest(5),然后再把它当初参数
ta.combine("5");//非法!!!,因为"5"是字符串字面值,或者说const char[],它无法通过一层转换,变成myStructTest类。除非让string s = "5";再让s作为实参
如果在只含一个参数的构造函数前面用explicit关键字进行声明,就能阻止隐式转换