101、从结构体到类
对面向对象编程来说,一切都是对象,对象用类来描述。
类把对象的数据和操作数据的方法作为一个整体考虑。
定义类的语法:
class 类名
{
public:
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
成员三的数据类型 成员名三;
...... 成员 n 的数据类型 成员名 n;
};
注意:
类的成员可以是变量,也可以是函数。
类的成员变量也叫属性。
类的成员函数也叫方法/行为,类的成员函数可以定义在类的外面。
用类定义一个类的变量叫创建(或实例化)一个对象。
对象的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同。
#include
using namespace std; // 指定缺省的命名空间。
struct st_girl // 超女基本信息结构体 st_girl,存放了超女全部的数据项。
{
string name; // 姓名。
int age; // 年龄。
int height; // 身高(cm)。
double weight; // 体重(kg)。
char sex='X'; // 性别:X-女;Y-男。
int yz; // 颜值:1-漂亮;2-一般;3-歪瓜裂枣。
string special; // 特长。
string memo; // 备注。
};
void setvalue(st_girl& girl, string name, int age, int height, double weight, char sex, int yz, string special, string memo)
{
girl.name = name;
girl.age = age;
girl.height = height;
girl.weight = weight;
girl.sex = sex;
girl.yz = yz;
girl.special = special;
girl.memo = memo;
}
void show(const st_girl& girl)
{
cout << "姓名:" << girl.name << ",年龄:" << girl.age << ",身高:" << girl.height
<< ",体重:" << girl.weight << ",性别:" << girl.sex << ",颜值:" << girl.yz
<< ",特长:" << girl.special << ",备注:" << girl.memo << endl;
}
int main()
{
st_girl girl;
setvalue(girl, "西施", 26, 170, 50.5, 'X', 1, "唱歌、跳舞、洗衣服。", "春秋第一美女,四大美女
之一。");
show(girl);
}
#include
using namespace std; // 指定缺省的命名空间。
struct st_girl // 超女基本信息结构体 st_girl,存放了超女全部的数据项。
{
string name; // 姓名。
int age; // 年龄。
void setvalue(string name1, int age1) // 设置成员变量的值。
{
name = name1; age = age1;
}
void show() // 显示超女的自我介绍。
{
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
int main()
{
st_girl girl; // 创建结构体变量。
girl.setvalue("西施", 26); // 设置成员变量的值。
girl.show(); // 显示超女的自我介绍。
}
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
string name; // 姓名。
int age; // 年龄。
void setvalue(string name1, int age1); // 设置成员变量的值。
void show() // 显示超女的自我介绍。
{
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
void CGirl::setvalue(string name1, int age1) // 设置成员变量的值。
{
name = name1; age = age1;
}
int main()
{
CGirl girl; // 创建超女对象。
girl.setvalue("西施", 26); // 设置成员变量的值。
girl.show(); // 显示超女的自我介绍。
}
102、类的访问权限
类的成员有三种访问权限:public、private 和 protected,分别表示公有的、私有的和受保护的。
在类的内部(类的成员函数中),无论成员被声明为 public 还是 private,都可以访问。
在类的外部(定义类的代码之外),只能访问 public 成员,不能访问 private、protected 成员。
在一个类体的定义中,private 和 public 可以出现多次。
结构体的成员缺省为 public,类的成员缺省为 private。
private 的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为 public。
103、简单使用类
编程思想和方法的改变,披着 C++外衣的 C 程序员。
1)类的成员函数可以直接访问该类其它的成员函数(可以递归)。
2)类的成员函数可以重载,可以使用默认参数。
3)类指针的用法与结构体指针用法相同。
4)类的成员可以是任意数据类型(类中枚举)。
5)可以为类的成员指定缺省值(C++11 标准)。
6)类可以创建对象数组,就像结构体数组一样。
7)对象可以作为实参传递给函数,一般传引用。
8)可以用 new 动态创建对象,用 delete 释放对象。
9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。数据隐藏是面向对象编
程的思想之一。
10)对象一般不用 memset()清空成员变量,可以写一个专用于清空成员变量的成员函数。
11)对类和对象用 sizeof 运算意义不大,一般不用。
12)用结构体描述纯粹的数据,用类描述对象。
13)在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了 inlin
e 限定符,也是内联函数。
14)为了区分类的成员变量和成员函数的形参,把成员变量名加 m_前缀或_后缀,如 m_name 或 n
ame_。
15)类的分文件编写。
104、构造函数和析构函数
构造函数:在创建对象时,自动的进行初始化工作。
析构函数:在销毁对象前,自动的完成清理工作。
1)构造函数
语法:类名(){......}
访问权限必须是 public。
函数名必须与类名相同。
没有返回值,不写 void。
可以有参数,可以重载,可以有默认参数。
创建对象时只会自动调用一次,不能手工调用。
2)析构函数
语法:~类名(){......}
访问权限必须是 public。
函数名必须在类名前加~。
没有返回值,也不写 void。
没有参数,不能重载。
销毁对象前只会自动调用一次,但是可以手工调用。
注意:
1) 如果没有提供构造/析构函数,编译器将提供空实现的构造/析构函数。
2) 如果提供了构造/析构函数,编译器将不提供空实现的构造/析构函数。
3) 创建对象的时候,如果重载了构造函数,编译器根据实参匹配相应的构造函数。没有参数的构造
函数也叫默认构造函数。
4) 创建对象的时候不要在对象名后面加空的圆括号,编译器误认为是声明函数。(如果没有构造函
数、构造函数没有参数、构造函数的参数都有默认参数)
5) 在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象。
6) 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值(可能会导致问题,不推荐)。
CGirl girl =10;
7) 以下两行代码有本质的区别:
CGirl girl = CGirl("西施"20); // 显式创建对象。
CGirl girl; // 创建对象。
girl = CGirl("西施"20); // 创建匿名对象,然后给现有的对象赋值。
8) 用 new/delete 创建/销毁对象时,也会调用构造/析构函数。
9) 不建议在构造/析构函数中写太多的代码,可以调用成员函数。
10) 除了初始化,不建议让构造函数做太多工作(只能成功不会失败)。
11) C++11 支持使用统一初始化列表。
CGirl girl = {"西施"20};
CGirl girl {"西施"20};
CGirl* girl = new CGirl{ "西施"20 };
12) 如果类的成员也是类,创建对象的时候,先构造成员类;销毁对象的时候,先析构成员类。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
string m_name; // 姓名属性。
int m_age; // 年龄属性。
char m_memo[301]; // 备注。
CGirl() // 没有参数的构造函数。
{
initdata();
cout << "调用了 CGirl()构造函数。\n";
}
CGirl(string name) // 一个参数(姓名)的构造函数。
{
initdata();
cout << "调用了 CGirl(name)构造函数。\n";
m_name = name;
}
CGirl(int age) // 一个参数(年龄)的构造函数。
{
initdata();
cout << "调用了 CGirl(age)构造函数。\n";
m_age = age;
}
CGirl(string name, int age) // 两个参数的构造函数。
{
initdata();
cout << "调用了 CGirl(name,age)构造函数。\n";
m_name = name; m_age = age;
}
void initdata()
{
m_name.clear(); m_age = 0; memset(m_memo, 0, sizeof(m_memo));
}~CGirl() // 析构函数。
{
cout << "调用了~CGirl()\n";
}
void show() // 超女自我介绍的方法。
{ cout << "姓名:" << m_name << ",年龄:" << m_age << ",备注:" << m_memo<<
endl; }
};
int main()
{
// CGirl girl; // 创建超女对象,不设置任何初始值。
// CGirl girl("西施"); // 创建超女对象,为成员姓名设置初始值。
// CGirl girl("西施",8); // 创建超女对象,为成员姓名和年龄设置初始值。
// CGirl girl=CGirl(); // 创建超女对象,不设置任何初始值。
// CGirl girl=CGirl("西施"); // 创建超女对象,为成员姓名设置初始值。
// CGirl girl=CGirl("西施",8); // 创建超女对象,为成员姓名和年龄设置初始值。
// CGirl girl = 8; // 使用赋值语法初始化对象。
// CGirl *girl=new CGirl; // 创建超女对象,不设置任何初始值。
// CGirl *girl=new CGirl("西施"); // 创建超女对象,为成员姓名设置初始值。
CGirl *girl=new CGirl("西施",8); // 创建超女对象,为成员姓名和年龄设置初始值。
girl->show(); // 显示超女的自我介绍。
delete girl;
}
105、拷贝构造函数
用一个已存在的对象创建新的对象,不会调用(普通)构造函数,而是调用拷贝构造函数。
如果类中没有定义拷贝构造函数,编译器将提供一个拷贝构造函数,它的功能是把已存在对象的成员
变量赋值给新对象的成员变量。
用一个已存在的对象创建新的对象语法:
类名 新对象名(已存在的对象名);
类名 新对象名=已存在的对象名;
拷贝构造函数的语法:
类名(const 类名& 对象名){......}
注意:
访问权限必须是 public。
函数名必须与类名相同。
没有返回值,不写 void。
如果类中定义了拷贝构造函数,编译器将不提供默认的拷贝构造函数。
以值传递的方式调用函数时,如果实参为对象,会调用拷贝构造函数。
函数以值的方式返回对象时,可能会调用拷贝构造函数(VS 会调用,Linux 不会,g++编译器
做了优化)。
拷贝构造函数可以重载,可以有默认参数。
类名(......,const 类名& 对象名,......){......}
如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造
函数。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
string m_name; // 姓名属性。
int m_age; // 年龄属性。
// 没有参数的普通构造函数。
CGirl() { m_name.clear(); m_age = 0; cout << "调用了 CGirl()构造函数。\n"; }
// 没有重载的拷贝构造函数(默认拷贝构造函数)。
CGirl(const CGirl &gg) { m_name="漂亮的"+gg.m_name; m_age = gg.m_age-1; cout
<< "调用了 CGirl(const CGirl &gg)拷贝构造函数。\n"; }
// 重载的拷贝构造函数。
CGirl(const CGirl& gg,int ii) { m_name = "漂亮的" + gg.m_name; m_age = gg.m_age - ii;
cout << "调用了 CGirl(const CGirl &gg,int ii)拷贝构造函数。\n"; }
// 析构函数。
~CGirl() { cout << "调用了~CGirl()\n"; }
// 超女自我介绍的方法,显示姓名和年龄。
void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << endl; }
};
int main()
{
CGirl g1;
g1.m_name = "西施"; g1.m_age = 23;
CGirl g2(g1,3);
g2.show();
}
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
string m_name; // 姓名属性。
int m_age; // 年龄属性。
int* m_ptr; // 指针成员,计划使用堆内存。
// 没有参数的普通构造函数。
CGirl() { m_name.clear(); m_age = 0; m_ptr = nullptr; cout << "调用了 CGirl()构造函数。
\n"; }
// 没有重载的拷贝构造函数(默认拷贝构造函数)。
CGirl(const CGirl& gg)
{
m_name = gg.m_name; m_age = gg.m_age;
m_ptr = new int; // 分配内存。
// *m_ptr = *gg.m_ptr; // 拷贝数据。
memcpy(m_ptr, gg.m_ptr, sizeof(int)); // 拷贝数据。
cout << "调用了 CGirl(const CGirl &gg)拷贝构造函数。\n";
}
// 析构函数。
~CGirl() { delete m_ptr; m_ptr = nullptr; cout << "调用了~CGirl()\n"; }
// 超女自我介绍的方法,显示姓名和年龄。
void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << ",m_ptr="<<
m_ptr<<",*m_ptr="<<*m_ptr<
int main()
{
CGirl g1;
g1.m_name = "西施"; g1.m_age = 23; g1.m_ptr = new int(3);
g1.show();
CGirl g2(g1); *g2.m_ptr = 8;
g1.show();
g2.show();
}
106、初始化列表
构造函数的执行可以分成两个阶段:初始化阶段和计算阶段(初始化阶段先于计算阶段)。
初始化阶段:全部的成员都会在初始化阶段初始化。
计算阶段:一般是指用于执行构造函数体内的赋值操作。
构造函数除了参数列表和函数体之外,还可以有初始化列表。
初始化列表的语法:
类名(形参列表):成员一(值一), 成员二(值二),..., 成员 n(值 n)
{......}
注意:
1)如果成员已经在初始化列表中,则不应该在构造函数中再次赋值。
2)初始化列表的括号中可以是具体的值,也可以是构造函数的形参名,还可以是表达式。
3)初始化列表与赋值有本质的区别,如果成员是类,使用初始化列表调用的是成员类的拷贝构造函
数,而赋值则是先创建成员类的对象(将调用成员类的普通构造函数),然后再赋值。
4)如果成员是类,初始化列表对性能略有提升。
5)如果成员是常量和引用,必须使用初始列表,因为常量和引用只能在定义的时候初始化。
6)如果成员是没有默认构造函数的类,则必须使用初始化列表。
7)拷贝构造函数也可以有初始化列表。
8)类的成员变量可以不出现在初始化列表中。
9)构造函数的形参先于成员变量初始化。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CBoy // 男朋友类。
{
public:
string m_xm; // 男朋友的姓名。
CBoy() // 没有参数的普通构造函数,默认构造函数。
{ m_xm.clear(); cout << "调用了 CBoy()构造函数。\n"; }
CBoy(string xm) // 有一个参数的普通构造函数。
{ m_xm = xm; cout << "调用了 CBoy(string xm)构造函数。\n"; }
CBoy(const CBoy& bb) // 默认拷贝构造函数。
{ m_xm = bb.m_xm; cout << "调用了 CBoy(const CBoy &bb)拷贝构造函数。\n"; }
};
class CGirl // 超女类 CGirl。
{
public:
string m_name; // 姓名属性。
const int m_age; // 年龄属性。
CBoy& m_boy; // 男朋友的信息。
//CGirl() // 没有参数的普通构造函数,默认构造函
数。
//{
// cout << "调用了 CGirl()构造函数。\n";
//}
//CGirl(string name, int age,CBoy &boy) // 三个参数的普通构造函数。
//{
// m_name = name; m_age = age; m_boy.m_xm = boy.m_xm;
// cout << "调用了 CGirl(name,age,boy)构造函数。\n";
//}
CGirl(string name, int age, CBoy& boy) :m_name(name), m_age(age),m_boy(boy)
// 三个参数的普通构造函数。
{
cout << "调用了 CGirl(name,age,boy)构造函数。\n";
}
// 超女自我介绍的方法,显示姓名、年龄、男朋友。
void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << ",男朋友:" <<
m_boy.m_xm << endl; }
};
int main()
{
CBoy boy("子都");
CGirl g1("冰冰",18,boy);
g1.show();
}
107、const 修饰成员函数
在类的成员函数后面加 const 关键字,表示在成员函数中保证不会修改调用对象的成员变量。
注意:
1)mutable 可以突破 const 的限制,被 mutable 修饰的成员变量,将永远处于可变的状态,在 co
nst 修饰的函数中,mutable 成员也可以被修改。
2)非 const 成员函数可以调用 const 成员函数和非 const 成员函数。
3)const 成员函数不能调用非 const 成员函数。
4)非 const 对象可以调用 const 修饰的成员函数和非 const 修饰的成员函数。
5)const 对象只能调用 const 修饰的成员函数,不能调用非 cosnt 修饰的成员函数。
这里出现了令人纠结的三个问题:
1、为什么要保护类的成员变量不被修改?
2、为什么用 const 保护了成员变量,还要再定义一个 mutable 关键字来突破 const 的封锁线?
3、到底有没有必要使用 const 和 mutable 这两个关键字?
保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用 const 关键字来避免
在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成
员函数的带来的影响。而 mutable 则是为了能突破 const 的封锁线,让类的一些次要的或者是辅助性的
成员变量随时可以被更改。没有使用 const 和 mutable 关键字当然没有错,const 和 mutable 关键字
只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和
建模工具去做,从而减轻程序员的负担。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
mutable string m_name; // 姓名属性。
int m_age; // 年龄属性。
// 两个参数的普通构造函数。
CGirl(const string &name, int age)
{ m_name = name; m_age = age; cout << "调用了 CGirl(name,age)构造函数。\n"; }
// 超女自我介绍的方法,显示姓名、年龄。
void show1() const
{
m_name="西施 show1";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
void show2() const
{
m_name = "西施 show2";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
void show3()
{
m_name = "西施 show3";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
void show4()
{
m_name = "西施 show4";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
};
int main()
{
const CGirl g1("冰冰",18);
g1.show1();
}
108、this 指针
如果类的成员函数中涉及多个对象,在这种情况下需要使用 this 指针。
this 指针存放了对象的地址,它被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用
者对象)。
每个成员函数(包括构造函数和析构函数)都有一个 this 指针,可以用它访问调用者对象的成员。
(可以解决成员变量名与函数形参名相同的问题)
*this 可以表示对象。
如果在成员函数的括号后面使用 const,那么将不能通过 this 指针修改成员变量。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
string m_name; // 姓名属性。
int m_yz; // 颜值:1-沉鱼落雁;2-漂亮;3-一般;4- 歪瓜裂枣。
// 两个参数的普通构造函数。
CGirl(const string &name, int yz) { m_name = name; m_yz = yz; }
// 超女自我介绍的方法。
void show() const { cout << "我是:" << m_name << ",最漂亮的超女。"<< endl; }
// 超女颜值 pk 的方法。
const CGirl& pk(const CGirl& g) const
{
if (g.m_yz < m_yz) return g;
return *this;
}
};
int main()
{
// 比较五个超女的颜值,然后由更漂亮的超女作自我介绍。
CGirl g1("西施",5), g2("西瓜",3), g3("冰冰", 4), g4("幂幂", 5), g5("金莲", 2);
const CGirl& g = g1.pk(g2).pk(g3).pk(g4).pk(g5);
g.show();
}
109、静态成员
类的静态成员包括静态成员变量和静态成员函数。
用静态成员可以变量实现多个对象之间的数据共享,比全局变量更安全性。
用 static 关键字把类的成员变量声明为静态,表示它在程序中(不仅是对象)是共享的。
静态成员变量不会在创建对象的时候初始化,必须在程序的全局区用代码清晰的初始化(用范围解析
运算符 ::)。
静态成员使用类名加范围解析运算符 :: 就可以访问,不需要创建对象。
如果把类的成员声明为静态的,就可以把它与类的对象独立开来(静态成员不属于对象)。
静态成员变量在程序中只有一份(生命周期与程序运行期相同,存放在静态存储区的),不论是否创
建了类的对象,也不论创建了多少个类的对象。
在静态成员函数中,只能访问静态成员,不能访问非静态成员。
静态成员函数中没有 this 指针。
在非静态成员函数中,可以访问静态成员。
私有静态成员在类外无法访问。
const 静态成员变量可以在定义类的时候初始化。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
static int m_age; // 年龄属性。
public:
string m_name; // 姓名属性。
// 两个参数的普通构造函数。
CGirl(const string& name, int age) { m_name = name; m_age = age; }
// 显示超女的姓名。
void showname() { cout << "姓名:" << m_name << endl; }
// 显示超女的年龄。
static void showage() { cout << "年龄:" << m_age << endl; }
};
int CGirl::m_age=8; // 初始化类的静态成员变量。
int main()
{
CGirl g1("西施 1", 21), g2("西施 2", 22), g3("西施 3", 23);
g1.showname(); g1.showage();
g2.showname(); g2.showage();
g3.showname(); g3.showage();
CGirl::showage();
// cout << "CGirl::m_age=" << CGirl::m_age << endl;
}
110、简单对象模型
在 C 语言中,数据和处理数据的操作(函数)是分开的。也就是说,C 语言本身没有支持数据和函数
之间的关联性。
C++用类描述抽象数据类型(abstract data type,ADT),在类中定义了数据和函数,把数据和
函数关联起来。
对象中维护了多个指针表,表中放了成员与地址的对应关系。
class CGirl // 超女类 CGirl。
{
public:
char m_name[10]; // 姓名属性。
int m_age; // 年龄属性。
// 默认构造函数和析构函数。
CGirl() { memset(m_name, 0, sizeof(m_name)); m_age = 0; } ~CGirl() { }
// 显示超女的姓名。
void showname() { cout << "姓名:" << m_name << endl; }
// 显示超女的年龄。
void showage() { cout << "年龄:" << m_age << endl; }
};
C++类中有两种数据成员:nonstatic、static,三种函数成员:nonstatic、static、virtual。
对象内存的大小包括:1)所有非静态数据成员的大小;2)由内存对齐而填补的内存大小;3)
为了支持 virtual 成员而产生的额外负担。
静态成员变量属于类,不计算在对象的大小之内。
成员函数是分开存储的,不论对象是否存在都占用存储空间,在内存中只有一个副本,也不计算
在对象大小之内。
用空指针可以调用没有用到 this 指针的非静态成员函数。
对象的地址是第一个非静态成员变量的地址,如果类中没有非静态成员变量,编译器会隐含的增
加一个 1 字节的占位成员。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
char m_name[3]; // 姓名属性。
int m_bh; // 编号属性。
static int m_age; // 年龄属性。
// 默认构造函数和析构函数。
CGirl() { memset(m_name, 0, sizeof(m_name)); m_age = 0; } ~CGirl() { }
// 显示超女的姓名。
void showname() { if (this == nullptr) return; cout << "姓名:" << this->m_name <<
endl; }
// 显示超女的年龄。
void showage() { cout << "年龄:" << m_age << endl; }
};
int CGirl::m_age;
int aaa;
void func() {}
int main()
{
CGirl g;
cout << "对象 g 占用的内存大小是:" << sizeof(g) << endl;
cout << "对象 g 的地址是:" << (void*)&g << endl;
cout << "成员变量 m_bh 的地址是:" << (void*)&g.m_bh << endl;
cout << "成员变量 m_name 的地址是:" << (void*)&g.m_name << endl;
cout << "成员变量 m_age 的地址是:" << (void *)&g.m_age << endl;
cout << "全局变量 aaa 的地址是:" << (void*)&aaa << endl;
printf("成员函数 showname 的地址是:%p\n", &CGirl::showname);
printf("成员函数 showage 的地址是:%p\n", &CGirl::showage);
printf("函数 func()的地址是:%p\n", func);
CGirl* g1 = nullptr;
g1->showname();
}
111、友元
如果要访问类的私有成员变量,调用类的公有成员函数是唯一的办法,而类的私有成员函数则无法访
问。
友元提供了另一访问类的私有成员的方案。友元有三种:
友元全局函数。
友元类。
友元成员函数。
1)友元全局函数
在友元全局函数中,可以访问另一个类的所有成员。
2)友元类
在友元类所有成员函数中,都可以访问另一个类的所有成员。
友元类的注意事项:
友元关系不能被继承。
友元关系是单向的,不具备交换性。
若类 B 是类 A 的友元,类 A 不一定是类 B 的友元。B 是类 A 的友元,类 C 是 B 的友元,类 C 不一
定是类 A 的友元,要看类中是否有相应的声明。
3)友元成员函数
在友元成员函数中,可以访问另一个类的所有成员。
如果要把男朋友类 CBoy 的某成员函数声明为超女类 CGirl 的友元,声明和定义的顺序如下:
class CGirl; // 前置声明。
class CBoy { ...... }; // CBoy 的定义。
class CGirl { ...... }; // CGirl 的定义。
// 友元成员函数的定义。
void CBoy::func(CGirl &g) { ...... }
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
friend int main();
friend void func();
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() { cout << "胸围:" << m_xw << endl; }
};
void func()
{
CGirl g;
g.showname();
g.showxw();
}
int main()
{
func();
}
///
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
friend class CBoy;
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() const { cout << "胸围:" << m_xw << endl; }
};
class CBoy // 超女的男朋友类
{
public:
void func(const CGirl& g)
{
cout << "我女朋友的姓名是:" << g.m_name << endl;
cout << "我女朋友的胸围是:" << g.m_xw << endl;
g.showxw();
}
};
int main()
{
CGirl g;
CBoy b;
b.func(g);
}
///
#include
using namespace std; // 指定缺省的命名空间。
class CGirl; // 把超女类的声明前置
class CBoy // 超女的男朋友类
{
public:
void func1(const CGirl& g);
void func2(const CGirl& g);
};
class CGirl // 超女类 CGirl。
{
friend void CBoy::func1(const CGirl& g);
// friend void CBoy::func2(const CGirl& g);
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() const { cout << "胸围:" << m_xw << endl; }
};
void CBoy::func1(const CGirl& g) { cout << "func1()我女朋友的胸围是:" << g.m_xw <<
endl; }
void CBoy::func2(const CGirl& g) { cout << "func2()我女朋友的姓名是:" << g.m_name <<
endl; }
int main()
{
CGirl g;
CBoy b;
b.func2(g);
b.func1(g);
}
112、运算符重载基础
C++将运算符重载扩展到自定义的数据类型,它可以让对象操作更美观。
例如字符串 string 用加号(+)拼接、cout 用两个左尖括号(<<)输出。
运算符重载函数的语法:返回值 operator 运算符(参数列表);
运算符重载函数的返回值类型要与运算符本身的含义一致。
非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同;
成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传
递了调用对象。
如果同时重载了非成员函数和成员函数版本,会出现二义性。
注意:
1)返回自定义数据类型的引用可以让多个运算符表达式串联起来。(不要返回局部变量的引用)
2)重载函数参数列表中的顺序决定了操作数的位置。
3)重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
4)如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算
符重载的初衷。
5)重载函数不能违背运算符原来的含义和优先级。
6)不能创建新的运算符。
7)以下运算符不可重载:
sizeof sizeof 运算符
. 成员运算符
.* 成员指针运算符
:: 作用域解析运算符
?: 条件运算符
typeid 一个 RTTI 运算符
const_cast 强制类型转换运算符
dynamic_cast 强制类型转换运算符
reinterpret_cast 强制类型转换运算符
static_cast 强制类型转换运算符
8)以下运算符只能通过成员函数进行重载:
= 赋值运算符
() 函数调用运算符
[] 下标运算符
-> 通过指针访问类成员的运算符
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
friend CGirl &operator+(CGirl& g, int score);
friend CGirl& operator+(int score, CGirl& g);
friend CGirl& operator+(CGirl& g1, CGirl& g2);
private:
int m_xw; // 胸围。
int m_score; // 分数。
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; m_score = 30; }
// 自我介绍的方法。
void show() { cout << "姓名:" << m_name << ",胸围:" << m_xw << ",评分:" <<
m_score << endl; }
//CGirl& operator-(int score) // 给超女减分的函数。
//{
// m_score = m_score - score;
// return *this;
//}
};
CGirl& operator+(CGirl& g, int score) // 给超女加分的函数。
{
g.m_score = g.m_score + score;
return g;
}
CGirl& operator+(int score,CGirl& g) // 给超女加分的函数。
{
g.m_score = g.m_score + score;
return g;
}
CGirl& operator+(CGirl& g1, CGirl& g2) // 给超女加分的函数。
{
g1.m_score = g1.m_score + g2.m_score;
return g1;
}
int main()
{
// 导演的要求:每轮表演之后,给超女加上她的得分。
CGirl g;
g = g+g;
g.show();
}
113、重载关系运算符
重载关系运算符(==、!=、>、>=、<、<=)用于比较两个自定义数据类型的大小。
可以使用非成员函数和成员函数两种版本,建议采用成员函数版本。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
string m_name; // 姓名。
int m_yz; // 颜值:1-千年美人;2-百年美人;3-绝代美人;4-极漂亮;
5-漂亮;6-一般;7-歪瓜裂枣。
int m_sc; // 身材:1-火辣;2-...;3-...;4-...;5-...;6-...;7-膘肥体壮。
int m_acting; // 演技:1-完美;2-...;3-...;4-...;5-...;6-...;7-四不像。
public:
// 四个参数的构造函数。
CGirl(string name, int yz, int sc, int acting) { m_name = name; m_yz = yz; m_sc = sc;
m_acting = acting; }
// 比较两个超女的商业价值。
bool operator==(const CGirl& g1) // 相等==
{
if ((m_yz + m_sc + m_acting) == (g1.m_yz + g1.m_sc + g1.m_acting)) return true;
return false;
}
bool operator>(const CGirl& g1) // 大于>
{
if ((m_yz + m_sc + m_acting) < (g1.m_yz + g1.m_sc + g1.m_acting)) return true;
return false;
}
bool operator<(const CGirl& g1) // 小于<
{
if ((m_yz + m_sc + m_acting) > (g1.m_yz + g1.m_sc + g1.m_acting)) return true;
return false;
}
};
int main()
{
CGirl g1("西施", 1, 2, 2), g2("冰冰", 1, 1, 1);
if (g1==g2)
cout << "西施和冰冰的商业价值相同。\n";
else
if (g1>g2)
cout << "西施商业价值相同比冰冰大。\n";
else
cout << "冰冰商业价值相同比西施大。\n";
}
114、重载左移运算符
重载左移运算符(<<)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)。
只能使用非成员函数版本。
如果要输出对象的私有成员,可以配合友元一起使用。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
friend ostream& operator<<(ostream& cout, const CGirl& g);
string m_name; // 姓名。
int m_xw; // 胸围。
int m_score; // 评分。
public:
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; m_score = 30; }
// 自我介绍的方法。
void show() { cout << "姓名:" << m_name << ",胸围:" << m_xw << ",评分:" <<
m_score << endl; }
};
ostream& operator<<(ostream& cout, const CGirl& g)
{
cout << "姓名:" << g.m_name << ",胸围:" << g.m_xw << ",评分:" << g.m_score;
return cout;
}
int main()
{
CGirl g;
cout << g << endl;
}
115、重载下标运算符
如果对象中有数组,重载下标运算符[],操作对象中的数组将像操作普通数组一样方便。
下标运算符必须以成员函数的形式进行重载。
下标运算符重载函数的语法:
返回值类型 &perator[](参数);
或者:
const 返回值类型 &operator[](参数) const;
使用第一种声明方式,[]不仅可以访问数组元素,还可以修改数组元素。
使用第二种声明方式,[]只能访问而不能修改数组元素。
在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应 const 对象,因为通过 const
对象只能调用 const 成员函数,如果不提供第二种形式,那么将无法访问 const 对象的任何数组元素。
在重载函数中,可以对下标做合法性检查,防止数组越界。
例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
private:
string m_boys[3]; // 超女的男朋友
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_boys[0] = "子都"; m_boys[1] = "潘安"; m_boys[2] = "宋玉"; }
// 显示全部男朋友的姓名。
void show() { cout << m_boys[0] << "、" << m_boys[1] << "、" << m_boys[2] << endl; }
string& operator[](int ii)
{
return m_boys[ii];
}
const string& operator[](int ii) const
{
return m_boys[ii];
}
};
int main()
{
CGirl g; // 创建超女对象。
g[1] = "王麻子";
cout << "第 1 任男朋友:" << g[1] << endl;
g.show();
const CGirl g1 = g;
cout << "第 1 任男朋友:" << g1[1] << endl;
}
116、重载赋值运算符
C++编译器可能会给类添加四个函数:
默认构造函数,空实现。
默认析构函数,空实现。
默认拷贝构造函数,对成员变量进行浅拷贝。
默认赋值函数, 对成员变量进行浅拷贝。
对象的赋值运算是用一个已经存在的对象,给另一个已经存在的对象赋值。
如果类的定义中没有重载赋值函数,编译器就会提供一个默认赋值函数。
如果类中重载了赋值函数,编译器将不提供默认赋值函数。
重载赋值函数的语法:类名 & operator=(const 类名 & 源对象);
注意:
编译器提供的默认赋值函数,是浅拷贝。
如果对象中不存在堆区内存空间,默认赋值函数可以满足需求,否则需要深拷贝。
赋值运算和拷贝构造不同:拷贝构造是指原来的对象不存在,用已存在的对象进行构造;赋值运
算是指已经存在了两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量。
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
int* m_ptr; // 计划使用堆区内存。
CGirl() { m_ptr = nullptr; } ~CGirl() { if (m_ptr) delete m_ptr; }
// 显示全部成员变量。
void show() { cout << "编号:" << m_bh << ",姓名:" << m_name << ",m_ptr=" <<
m_ptr <* ",*m_ptr=" << *m_ptr<< */endl; }
CGirl& operator=(const CGirl& g)
{
if (this == &g) return *this; // 如果是自己给自己赋值。
if (g.m_ptr == nullptr) // 如果源对象的指针为空,则清空目标对象的内存和指针。
{
if (m_ptr != nullptr) { delete m_ptr; m_ptr = nullptr; }
}
else // 如果源对象的指针不为空。
{
// 如果目标对象的指针为空,先分配内存。
if (m_ptr == nullptr) m_ptr = new int;
// 然后,把源对象内存中的数据复制到目标对象的内存中。
memcpy(m_ptr, g.m_ptr, sizeof(int));
}
m_bh = g.m_bh; m_name = g.m_name;
cout << "调用了重载赋值函数。\n" << endl;
return *this;
}
};
int main()
{
CGirl g1, g2; // 创建超女对象。
g1.m_bh = 8; g1.m_name = "西施"; g1.m_ptr = new int(3);
g1.show();
g2.show();
g2 = g1;
g2.show();
cout << "*g1.m_ptr=" << *g1.m_ptr << ",*g2.m_ptr=" << *g2.m_ptr << endl;
}
117、重载 new&delete 运算符
重载 new 和 delete 运算符的目是为了自定义内存分配的细节。(内存池:快速分配和归还,无碎片)
建议先学习 C 语言的内存管理函数 malloc()和 free()。
在 C++中,使用 new 时,编译器做了两件事情:
1)调用标准库函数 operator new()分配内存;
2)调用构造函数初始化内存;
使用 delete 时,也做了两件事情:
1)调用析构函数;
2)调用标准库函数 operator delete()释放内存。
构造函数和析构函数由编译器调用,我们无法控制。
但是,可以重载内存分配函数 operator new()和释放函数 operator delete()。
1)重载内存分配函数的语法:void* operator new(size_t size);
参数必须是 size_t,返回值必须是 void*。
2)重载内存释放函数的语法:void operator delete(void* ptr)
参数必须是 void *(指向由 operator new()分配的内存),返回值必须是 void。
重载的 new 和 delete 可以是全局函数,也可以是类的成员函数。
为一个类重载 new 和 delete 时,尽管不必显式地使用 static,但实际上仍在创建 static 成员函数。
编译器看到使用 new 创建自定义的类的对象时,它选择成员版本的 operator new()而不是全局版本
的 new()。
new[]和 delete[]也可以重载。
示例:
#include
using namespace std; // 指定缺省的命名空间。
void* operator new(size_t size) // 参数必须是 size_t(unsigned long long),返回值必须是
void*。
{
cout << "调用了全局重载的 new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是 void *,返回值必须是 void。
{
cout << "调用了全局重载的 delete。\n";
if (ptr == 0) return; // 对空指针 delete 是安全的。
free(ptr); // 释放内存。
}
class CGirl // 超女类 CGirl。
{
public:
int m_bh; // 编号。
int m_xw; // 胸围。
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数 CGirl()\n"; } ~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) // 参数必须是 size_t(unsigned long long),返回值必
须是 void*。
{
cout << "调用了类的重载的 new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是 void *,返回值必须是 void。
{
cout << "调用了类的重载的 delete。\n";
if (ptr == 0) return; // 对空指针 delete 是安全的。
free(ptr); // 释放内存。
}
};
int main()
{
int* p1 = new int(3);
cout << "p1=" << (void *)p1 <<",*p1=" <<*p1<< endl;
delete p1;
CGirl* p2 = new CGirl(3, 8);
cout << "p2 的地址是:" << p2 << "编号:" << p2->m_bh << ",胸围:" << p2->m_xw
<< endl;
delete p2;
}
内存池示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
int m_bh; // 编号。
int m_xw; // 胸围。
static char* m_pool; // 内存池的起始地址。
static bool initpool() // 个初始化内存池的函数。
{
m_pool = (char*)malloc(18); // 向系统申请 18 字节的内存。
if (m_pool == 0) return false; // 如果申请内存失败,返回 false。
memset(m_pool, 0, 18); // 把内存池中的内容初始化为 0。
cout << "内存池的起始地址是:" << (void*)m_pool << endl;
return true;
}
static void freepool() // 释放内存池。
{
if (m_pool == 0) return; // 如果内存池为空,不需要释放,直接返回。
free(m_pool); // 把内存池归还给系统。
cout << "内存池已释放。\n";
}
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数 CGirl()\n"; } ~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) // 参数必须是 size_t(unsigned long long),返回值必
须是 void*。
{
if (m_pool[0] == 0) // 判断第一个位置是否空闲。
{
cout << "分配了第一块内存:" << (void*)(m_pool + 1) << endl;
m_pool[0] = 1; // 把第一个位置标记为已分配。
return m_pool + 1; // 返回第一个用于存放对象的址。
}
if (m_pool[9] == 0) // 判断第二个位置是否空闲。
{
cout << "分配了第二块内存:" << (void*)(m_pool + 9) << endl;
m_pool[9] = 1; // 把第二个位置标记为已分配。
return m_pool + 9; // 返回第二个用于存放对象的址。
}
// 如果以上两个位置都不可用,那就直接系统申请内存。
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是 void *,返回值必须是 void。
{
if (ptr == 0) return; // 如果传进来的地址为空,直接返回。
if (ptr == m_pool + 1) // 如果传进来的地址是内存池的第一个位置。
{
cout << "释放了第一块内存。\n";
m_pool[0] = 0; // 把第一个位置标记为空闲。
return;
}
if (ptr == m_pool + 9) // 如果传进来的地址是内存池的第二个位置。
{
cout << "释放了第二块内存。\n";
m_pool[9] = 0; // 把第二个位置标记为空闲。
return;
}
// 如果传进来的地址不属于内存池,把它归还给系统。
free(ptr); // 释放内存。
}
};
char* CGirl::m_pool = 0; // 初始化内存池的指针。
int main()
{
// 初始化内存池。
if (CGirl::initpool()==false) { cout << "初始化内存池失败。\n"; return -1; }
CGirl* p1 = new CGirl(3, 8); // 将使用内存池的第一个位置。
cout << "p1 的地址是:" << p1 << ",编号:" << p1->m_bh << ",胸围:" << p1->m_xw
<< endl;
CGirl* p2 = new CGirl(4, 7); // 将使用内存池的第二个位置。
cout << "p2 的地址是:" << p2 << ",编号:" << p2->m_bh << ",胸围:" << p2->m_xw
<< endl;
CGirl* p3 = new CGirl(6, 9); // 将使用系统的内存。
cout << "p3 的地址是:" << p3 << ",编号:" << p3->m_bh << ",胸围:" << p3->m_xw
<< endl;
delete p1; // 将释放内存池的第一个位置。
CGirl* p4 = new CGirl(5, 3); // 将使用内存池的第一个位置。
cout << "p4 的地址是:" << p4 << ",编号:" << p4->m_bh << ",胸围:" << p4->m_xw
<< endl;
delete p2; // 将释放内存池的第二个位置。
delete p3; // 将释放系统的内存。
delete p4; // 将释放内存池的第一个位置。
CGirl::freepool(); // 释放内存池。
}
118、重载括号运算符
括号运算符()也可以重载,对象名可以当成函数来使用(函数对象、仿函数)。
括号运算符重载函数的语法:
返回值类型 operator()(参数列表);
注意:
括号运算符必须以成员函数的形式进行重载。
括号运算符重载函数具备普通函数全部的特征。
如果函数对象与全局函数同名,按作用域规则选择调用的函数。
函数对象的用途:
1)表面像函数,部分场景中可以代替函数,在 STL 中得到广泛的应用;
2)函数对象本质是类,可以用成员变量存放更多的信息;
3)函数对象有自己的数据类型;
4)可以提供继承体系。
示例:
#include
using namespace std; // 指定缺省的命名空间。
void show(string str) // 向超女表白的函数。
{
cout << "普通函数:" << str << endl;
}
class CGirl // 超女类。
{
public:
void operator()(string str) // 向超女表白的函数。
{
cout << "重载函数:" << str << endl;
}
};
int main()
{
CGirl show;
::show("我是一只傻傻鸟。");
show("我是一只傻傻鸟。");
}
119、重载一元运算符
可重载的一元运算符。
1)++ 自增 2)-- 自减 3)! 逻辑非 4)& 取地址
5)~ 二进制反码 6)* 解引用 7)+ 一元加 8) - 一元求反
一元运算符通常出现在它们所操作的对象的左边。
但是,自增运算符++和自减运算符--有前置和后置之分。
C++ 规定,重载++或--时,如果重载函数有一个 int 形参,编译器处理后置表达式时将调用这个重
载函数。
成员函数版:CGirl &operator++(); // ++前置
成员函数版:CGirl operator++(int); // 后置++
非成员函数版:CGirl &operator++(CGirl &); // ++前置
非成员函数版:CGirl operator++(CGirl &,int); // 后置++
示例:
#include
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
string m_name; // 姓名。
int m_ranking; // 排名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_ranking = 5; }
// 自我介绍的方法。
void show() const { cout << "姓名:" << m_name << ",排名:" << m_ranking << endl; }
CGirl & operator++() // ++前置的重载函数。
{
m_ranking++; return *this;
}
CGirl operator++(int) // ++后置的重载函数。
{
CGirl tmp = *this;
m_ranking++;
return tmp;
}
};
int main()
{
CGirl g1,g2; // 创建超女对象。
int ii=5 , jj=5;
int xx = ++(++(++ii)); cout << "xx=" << xx << ",ii=" << ii << endl;
int yy = jj++; cout << "yy=" << yy << ",jj=" << jj << endl;
CGirl g3 = ++(++(++g1)); cout << "g3.m_ranking=" << g3.m_ranking <<
",g1.m_ranking=" << g1.m_ranking << endl;
CGirl g4 = g2++; cout << "g4.m_ranking=" << g4.m_ranking <<
",g2.m_ranking=" << g2.m_ranking << endl;
// g2.show();
}