类
基本思想就是抽象和封装。
一个例子
struct Sales_data{
public:
std::string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data& );
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
this指针
在调用一个对象的成员函数时,会通过名为this的隐藏参数来访问调用它的那个对象。也就是说this是一个指针,指向调用它的那个对象。
Sales_data s;
s.isbn(); //此时this指向对象s
此外,this是一个常量指针,不能改变this所指向的对象。
上面例子中还有const成员函数 std::string isbn() const、double avg_price() const。
函数后面的const的作用是修改this指针的类型,因为this指针的类型默认是指向类类型的非常量版本的常量指针,在上面的例子中的this指针的类型是Sales_data* const,也就是说,如果对象是const类型的化,就无法在常量对象上调用普通的成员函数,所以,在成员函数后面加上const就可以访问常量对象的成员函数。后面带有const的成员函数称为常量成员函数。
常量对象、常量对象的指针或者引用只能调用常量成员函数,而且常量成员函数不能修改数据成员。
例外:对于可变数据成员,它永远不会是const的,所以常量成员函数可以修改mutable成员。
class Screen{
public:
void some_member() const;
private:
mutable size_t = mutbl;
};
void Screen::some_member(){
++mutbl;
}
这种情况下,虽然some_member是常量成员函数,但它仍然可以修改mutable成员。
返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data& rhs){
uints_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this; //返回了调用该函数的对象
}
比如像下面这样调用时,返回的*this就是total。
//total, trans都是Sales_data对象
total.combine(trans);
访问控制与封装
public:定义在其后的成员在整个程序内可以被访问。
private:定义在其后的成员可以被类内函数访问,不能被外部访问。
protected:///
class和struct的区别
唯一区别就是struct默认是public的,class默认是private的。也就是说struct内部可以有private成员。
友元
在类内部声明一个以friend开始的函数声明语句即可。友元不具有传递性和对称性。
void print(){
A a;
cout << a.value << endl;
}
class A{
private:
int value = 0;
public:
friend void print();
};
友元不受所在区域的访问控制级别的约束!!
一般要在类外对友元函数进行声明。
还可以把其他类定义成友元类,把其他类的成员函数定义成友元函数。例如:
class Window_mgr{
...
};
class Screen{
friend class Window_mgr;
//
...
};
此时,友元类的成员函数可以访问Screen类中所有成员。
class Window_mgr{
void clear();
};
class Screen{
//void Window_mgr::clear()必须在Screen之前被声明
friend void Window_mgr::clear();
//
...
};
对于类和非成员函数来说,声明不一定必须在友元声明之前。只有类的成员函数有这个要求。
不同版本的重载函数属于对于一个类来说属于不同的友元函数,需要在类中分别声明。
友元函数甚至还可以被定义在类内部,但是依然需要在类外声明该函数。
构造函数
不同于其他成员函数,构造函数不能被声明为const成员函数。一个对象只有在构造函数完成初始化后才具有常量属性。所以构造函数可以对常量对象写值。
生成的默认构造函数不适用的情况
- 已经手动声明了其他构造函数,这种情况下编译器就不会再生成默认构造函数。
- 含有内置类型或者复合类型的成员没有全部被赋予类内初始值时,若使用默认构造函数构造可能会得到非定义的值。
- 如果类中包含其他类类型成员,并且这个类类型没有默认构造函数时,编译器将无法初始化该成员。对于这样的类就必须自定义默认构造函数。
构造函数初始值列表
如果没有在构造函数初始值列表显式地初始化成员,那么数据成员将在执行构造函数体之前执行默认初始化。
Sales_data::Sales_data(const string& s, unsigned cnt, double price){
//在执行函数体之前,数据成员先被默认初始化,之后再被赋值
bookNo = s; //赋值
units_sold = cnt;
revenue = cnt * price;
}
上述方式一般情况下可行,但是以下两种情况下不可行,必须使用构造函数初始化列表:
- 数据成员是const或者引用的话,必须初始化。
- 数据成员属于某种类类型,且该类没有默认构造函数。
class ConstRef{
public:
ConstRef(int il);
private:
int i;
const int ci;
int& ri;
};
ConstRef::ConstRef(int il){
//此处是赋值
i = il; //正确
ci = il; //错误,const不能赋值
ri = &il; //错误,引用没有被初始化
}
//正确的构造函数
ConstRef::ConstRef(int il) : i(il), ci(il), ri(&il){ }
构造函数的初始值列表只用于说明初始化成员的值,不限定初始化的执行顺序。
一般的数据成员的初始化顺序与在类中出现的定义顺序一样,先定义的先被初始化。
有一个特殊情况需要注意:用类的一个成员来初始化另一个成员。
class X{
int i;//先定义i,在定义j。
int j;
public:
X(int val) : j(val), i(j) {} //错误,i先被定义,此时j未定义,用未定义的值初始化i不合法
};
委托构造函数
C++11新标准
委托构造函数内,成员初始值列表只有一个入口–类名本身。类名后紧跟圆括号括起来的参数列表,且参数列表必须与另外一个构造函数参数相匹配。
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) {}
Sales_data(std::istream &is) : Sales_data() {read(is, *this)}
};
其中,后面三个是委托构造函数,他们分别委托其他的构造函数帮忙进行构造。执行顺序是:先执行受委托的构造函数的初始化列表,接下来执行受委托的构造函数的函数体,最后再执行委托构造函数的函数体。
隐式的类类型转换
对于自定义的类类型,以下两种情况会发生隐式的类型转换:
- 类的构造函数只有一个形参,或者其他形参有默认值,则该类可能会发生隐式类型转换。
- 类定义了operator type()函数,会隐式地将类转换成type类型。
class Integer{
public:
Integer() : m_value(0) {}
Integer(int value) : m_value(value){
cout << "Integer(int)" << endl;
}
//Integer->int
operator int(){
cout << "operator int()" << endl;
return m_value;
}
private:
int m_value;
}
Integer(int)构造函数可以将int隐式转换为Integer类型,operator int()可以将Integer隐式转换成int。
int main(){
Integer value1;
value1 = 10; //调用Integer(int)将10转换成Integer类型,输出Integer(int)
cout << "value1 = " << value1 << endl;//会调用operator int()将Integer转换成int输出,内容 operator int() 换行 10
cout << "*********" << endl;
int value2 = value1; //输出第二个operator int()
cout << "value2 = " << value2 << endl;
return 0;
}
结果是:
Integer(int)
operator int()
value1 = 10
*********
operator int()
value2 = 10
此外,只允许一步转换,也就是说不能隐式地转换成一种类型然后再隐式转换成另外一种类型。
如果要防止构造函数隐式转换,需要将构造函数声明为explicit的。
如果上面的Integer类声明成下面形式,那么value1 = 10就无法编译通过。
class Integer{
public:
Integer() : m_value(0) {}
explicit Integer(int value) : m_value(value){
cout << "Integer(int)" << endl;
}
//Integer->int
operator int(){
cout << "operator int()" << endl;
return m_value;
}
private:
int m_value;
}
注意,explicit只对有一个实参的构造函数有效,并且只能出现在类内的构造函数声明中,在类外的构造函数定义时不能出现。
explicit构造函数只能用于直接初始化
class Sales_data{
public:
//explicit只出现在类内,若有类外定义不用写
explicit Sales_data(const std::string&s) : bookNo(s) {}
private:
std::string bookNo;
};
std::string book = "mybook";
Sales_data item1(book); //正确,这是直接初始化
Sales_data item2 = book; //错误,explicit构造函数不能用于拷贝形式的初始化
这是因为执行拷贝形式的初始化时,可能会发生类型隐式转换。
拷贝、赋值和析构
初始化自定义类型变量、以值的方式传递或者返回一个对象时对象会被拷贝。
使用赋值运算符会发生对象的赋值操作。
对象不再存在时执行销毁操作。
某些类不能依赖生成的默认拷贝构造函数、默认赋值运算符以及默认的析构函数
管理动态内存的类通常不能依赖上述版本的函数。
但是包含有vector、string等标准库容器成员的类可以使用默认生成的函数。
聚合类
条件
- 类的所有成员都是public的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,没有virtual函数。
struct Data{
int val;
string s;
};
可以用花括号括起来的列表来初始化聚合类的成员,且初始值的顺序必须与声明的顺序一致。
字面值常量类
数据成员都是字面值类型的聚合类是字面值常量类。
若一个类不是聚合类,但满足下面条件,那也是字面值常量类。
- 数据成员都必须是字面值常量。
- 类至少含有一个constexpr构造函数。
- 若一个数据成员含有类内初始值,则内置类型成员的初始值必须是一个常量表达式。或者如果成员属于某一类类型,那么初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr函数的参数和返回值必须是常量表达式,且constexpr函数默认是内联函数。
constexpr构造函数
尽管构造函数不能是const和private的,但是可以是constexpr的。constexpr函数既要满足构造函数不包含返回语句的要求,又要满足constexpr函数的唯一可以执行语句就是返回语句的要求。所以一般情况下,其函数体是空的
对于一个类来说,编译分为两步,先编译成员的声明,然后才编译成员函数体(如果有的话。)
这就是为什么成员函数体内可以时候后面声明的其他成员的原因。
对象初始化顺序
class C : public A, B{
C(int i, int j) : B(i), A(j){}
D d;
E e;
};
基类的初始化顺序按照声明的顺序,成员的初始化也是按照声明的顺序。
初始化顺序是A,B,D,E,C的构造函数。对于C(int i, int j) : B(i), A(j){},尽管B在前,但是不管用。
类的静态成员
静态成员可以是public和private的。类的静态成员独立于任何对象之外,对象中不包含任何与静态数据成员相关的数据。静态数据成员函数不予任何对象绑定,也就是说没有this指针也不能使用this指针,且不能是const的。
class Account{
public:
//成员函数不用作用域运算符就能直接访问静态成员。
void calculate() {amount += amount * interestRate;}
static double rate() {return interestRate;}
static void rate() {double};
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
类的静态成员使用方式
- 类名 + 域运算符直接访问静态成员。
- 对象、引用或者指针访问静态成员。
- 成员函数不用作用域运算符就能直接访问静态成员。
double r = Account::rate(); //类名+作用域运算符
Account ac1;
Account *ac2 = ac1;
r = ac1.rate(); //通过对象调用
r = ac2->rate(); //通过指针调用
在类外定义静态成员函数时,static只出现在类内部的成员函数声明中,不能出现在类外。
virtual也是。(还有哪些关键字也是这样??)
静态成员不能在类内部初始化,不能由构造函数初始化,必须在类的外部定义和初始化静态成员。一旦被定义,就存在程序的整个声明周期当中。
虽不能在构造函数内初始化,但任然能在构造函数内修改。
定义静态成员的方式
//定义并初始化一个静态成员
double Account ::interestRate = initRate();
//从类名开始,语句的剩余部分就都位于类的作用域之内了,所以initRate()前不用加类名。
//类内的成员可以访问类的私有成员。
静态成员的类内初始化
一般情况下静态成员不应在类内初始化。但是仍然可以为静态成员提供const整数类型的类内初始值。要求静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式。
class Account{
public:
static double rate();
static void rate();
private:
static constexpr int period = 30; //period是常量表达式
double daily_tbl[period]; //可以用初始化了的静态成员指定数组维度
};
如果在类的内部提供了一个初始值,则类外的定义就不能在指定初始值。
constexpr int Account::period; //不能再提供初始值
静态成员适用而非静态成员不适用的情景
静态数据成员可以是不完全类型。
静态数据成员类型可以是它所属的类类型,而非静态成员只能声明成它所属类类型的指针或者引用。
class Bar{
public:
//...
private:
static Bar mem1; //正确,静态成员可以是不完全类型
Bar* mem2; //正确,指针成员可以是不完全类型
Bar mem3; //错误,数据成员必须是完全类型
};
静态数据成员可以作为默认实参,而非静态成员不行。
class Screen{
public:
Screen& clear(char = bkground);
private:
static const char bkground;
};