介绍
C++Primer第七章学习心得
一、定义抽象数据类型
如果成员函数不改变成员变量,可以将该函数定义为常量成员函数,const放在形参之后,用于修饰this指针。当成员函数需要访问成员变量时,会隐式调用this指针,当原对象为常量,调用非const成员函数时,this指针指向非常量,因此会由于类型不匹配无法调用,而常量成员函数可以正常调用。对于非常量对象而言,则无影响。
class A
{
public:
A(int num) : n(num){};
int getN() const { return n; }
int getN() { return n; }
private:
int n;
};
int main(){
const A conA(1);
A b(2);
cout << conA.getN() << endl;//调用常量成员函数,若不定义const则调用失败
cout << b.getN() << endl;//调用非常量成员函数
}
成员函数声明在类中,实现在类中则是内联函数,也可以实现在类外,但需要加上类名作为命名空间。对于成员函数而言,可以直接访问类中的成员变量,通过隐含的常量指针this实现,在调用时将对象的地址赋给了this指针,不能改变this指向的对象。
编译器处理类时,先编译成员变量,再编译成员成员函数,因此成员变量可以出现在任何位置,都能被成员函数访问。
1.构造函数
只要类被创建,都会执行构造函数,由于构造函数执行在对象创建之前,因此可以对const成员变量写入,且构造函数本身不需要返回类型,也不能被声明成const。每个类都有一个合成的默认构造函数,前提是没有自定义构造函数,或者使用了 = default。一旦定义了构造函数,就要控制所有情况下创建对象的过程。
2.构造函数初始值列表:
在形参和函数块之间通过 :和成员名()实现
Sales_data (const std::string&s,unsigned n, double p):bookNo(s),units_sold(n),revenue(p*n) { }
二、访问控制与封装
通过访问说明符加强类的封装性
public后的成员可以被外部访问
private只能被类内成员访问
class和struct定义类的唯一区别就是默认访问权限,struct的默认为public,class的默认为private
如果设定成员变量为private,那么某些非成员函数想要使用类内成员就无法正常编译,可以设定这些函数为友元函数,在类内增加friend关键字作为函数声明即可,不受访问说明符限制,通常在类开头或者结尾集中声明友元。但是,类中的友元声明并不是真正的函数声明,还需要在类外独立声明函数,最好和类放在一个头文件中。
三、类的其他特性
1.类型成员
可以在public或者private中通过typedef或者using来为类型设置别名,作为类型成员,只能在定义之后使用,也会受到访问说明符影响。
2.内联函数
类中实现的函数是隐式的内联函数,也可以加上声明时加上关键字inline来显式声明内联函数,甚至可以声明时不加,在类外实现时加上关键字inline同样可以设置为内联函数,当然同时加上inline也不错误。
3.可变数据成员
成员变量前加关键字mutable,哪怕是const函数都能够修改它的值
4.返回*this的成员函数
函数类型为引用类型,返回值为指针,这样返回的是一个左值而非拷贝的右值
如果函数类型是const函数,那么非常量指针this会被转换为指向常量的指针,因此可以通过重载写出两个同名函数,并根据常量、非常量对象调用时自动选择合适的函数
class Screen{
public:
Screen &display(std::ostream &os){do_display(os);return *this;}
const Screen &display(std::ostream &os){do_display(os);return *this;}
private:
void do_display(std::ostream &os) const { os<<contents; }
}
Screen myScreen(5,3);
const Srceen blank(5,3);
myScreen.set('#').display(cout);//非常量对象调用非常量函数
blank.display(cout);//常量对象调用常量函数,返回指向常量的指针。
5.友元
除了普通的非成员函数可以做友元,其他类及其中的成员函数同样可以是友元,声明friend即可,但不具有传递性,不能被继承,且重载函数需要分别声明友元。由于友元不是类中成员,不能通过this指针访问,所以通常将对象作为友元的形参方便访问。
1.其他普通函数
在类中声明friend,在类外或者类中定义即可。如果要在类中调用友元函数,则该函数必须先被声明。
class A{
friend void Print(const A &a);//声明友元函数,通过形参访问
public:
A(string str,int num):name(str),age(num){}
private:
string name;
int age;
};
void Print(const A &a){
std::cout<<a.name<<" "<<a.age<<endl;
}
int main()
{
A a("H",25);
Print(a);
return 0;
}
2.其他类
在A类中声明friend class B;可以将B类设为友元,B中的所有成员都可以访问A类中的成员。且可以声明B在前,定义B在后。
class A{
friend class B;
public:
A(string str,int num):name(str),age(num){}
const void print()const{std::cout<<name<<" "<<age<<endl;}
private:
string name;
int age;
};
class B{
public:
void print(const A &a);
};
void B::print(const A &a){
std::cout<<a.name<<" "<<a.age<<endl;
}
int main()
{
A a("H",25);
B b;
b.print(a);
return 0;
}
3.其他类中的成员函数
如果将其他类的成员函数作为友元函数,则声明顺序和定义顺序有要求,必须先简单声明本身的类,再声明作为友元的类及其成员函数(不能定义),再完整声明本身的类,再定义友元的成员函数。
class A;//简单声明
class B{
public:
void print(const A &a);//友元函数声明不定义
};
class A{//完整声明
public:
A(string str,int num):name(str),age(num){}
friend void B::print(const A &a);//设置友元
private:
string name;
int age;
};
void B::print(const A &a){//定义友元函数
std::cout<<a.name<<" "<<a.age<<endl;
}
int main()
{
A a("H",25);
B b;
b.print(a);//通过调用B的友元函数访问A的成员变量
return 0;
}
定义在类中的友元是内联的。
四、类的作用域
一旦遇到类名,则定义剩余部分就在了的作用域之内,包括参数列表和函数体
在类中,先编译类中成员声明,最后才处理成员函数
typedef double Money;//第一步
string bal;//第二步
class Account{
public:
Money balance(){return bal;}//第四步,此时返回的是类中的bal
private:
Money bal;//第三步
};
为了避免某些情况的误解,最好不要重名
五、构造函数再探
1.构造函数列表初始化
最好使用列表初始化,而非赋值。主要是针对某些特定类型例如const或者引用。初始化顺序仅取决于类中成员变量的出现顺序,与列表中出现顺序无关,尤其是涉及用一个成员变量初始化另一个成员变量时。
2.可以用默认实参来实现默认构造函数
class A{
public:
A(int n):num(n){}
A() = default;//手动设定默认
private:
int num;
}
class A{
public:
A(int n = 0):num(n){}//默认实参
private:
int num;
}
3.委托构造函数
可以使用类中其他构造函数执行自己的初始化过程,与列表初始化类似。
class A{
A(int x,char y,string z):a(x),y(b),c(z){}
A():A(0,'',""){}//委托三参数版本实现默认构造
A(string z):A(0,'',z){}//委托三参数实现单参数
private:
int a;
char b;
string c;
}
如果三参数版本函数体不为空,则发生委托时,受委托的函数体会先被执行后才执行到委托者的函数体。
4.隐式类型转换
如果构造函数只接受一个实参,则该函数可以被认为是转换构造函数,是一种隐式类型转换。
class A{
public:
A(int x,char y,string z):a(x),y(b),c(z){}
A():A(0,'',""){}//委托三参数版本实现默认构造
A(string z):A(0,'',z){}//委托三参数实现单参数
void combine(A &a){c+=a.c;}
private:
int a;
char b;
string c;
}
string str = "XYZ";
A a(1,2,"ABC");
a.combine(str);//通过隐式类型转换将string转换为类A的一个对象
a.combine("MN");//错误,此处需要两步转换
a.combine(string("MN"));//正确,显式转换string后隐式转换类
a.combine(A("MN"));//正确,隐式转化string,显示转换类
为保证数据有效性,有时需要抑制类的隐式转换,通过关键词explicit,还能起到抑制拷贝初始化
构造函数vector name(n)就是explicit的
explicit A(string z):A(0,'',z){}//在类中声明时
a.combine(string("MN"));//错误,隐式转换类被抑制
a.combine(A("MN"));//正确,隐式转化string,显示转换类
A b = string("MN");//错误,explicit抑制拷贝初始化
5.聚合类
条件:
(1)所有成员public
(2)无构造函数
(3)无类内初始值
(4)没有基类和虚函数
可以通过列表初始化,但必须遵守实参顺序和定义顺序一致,若实参个数少于形参,则后面的参数被值初始化
struct Data{
int val;
string s;
};
Data val1{24,"Anna"};
Data val2 = {25,"Bob"};
Data val3 = {"Candy",20};//错误
6.字面值常量类
(待补充)
六、类的静态成员
成员与类相关,所有对象共享该成员,可以使用static,当静态成员为函数时,由于所有对象共享,因此该函数内无法使用this,也不能声明const,只能访问静态成员和类外部的其他函数。
访问静态成员使用作用域运算符,或者通过对象来访问,
class Account{
public:
void calculate(){amount+=amount*interestRate;}//成员函数可以直接使用静态成员
static double rate(){return interestRate;}
static void rate(double);//声明加static
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
void Account::rate(double newRate){//类外不需要再次static,普通函数定义即可
interestRate = newRate;
}
double r = Account::rate();
Account ac1,*ac2;
ac2 = &ac1;
r = ac1.rate();
r = ac2->rate();
静态数据成员不是创建类的对象时通过构造函数被定义的,因此必须在类的外部定义和初始化静态成员,且只能定义一次。静态数据成员的定义(初始化)不应该被放在头文件中(而是在相应的cpp文件中)
在类内初始化时,静态成员是常量表达式可以初始化为const整数类型,但尽量不要使用。
static constexpr int period = 30;
静态数据成员可以作为成员函数的默认实参,且能定义为自身的不完全类型,这是普通成员无法做到的。
总结
关于类的构造函数、友元的不熟悉的地方比较多,还是得通过一些实际的例子来感受他们的作用。