类是对象的模板,类是抽象的——对象的抽象,对象是具体的——是类的实例
C++可以以这种方式写成员访问限定符
class 类名
{ private:
私有的数据和成员函数;
public:
公用的数据和成员函数;
};
//声明对象:类名 对象名
Student stud1, stud2;
类和结构体类型的区别:
用 struct 声明的类,如果对全部成员不作 private 和public 的声明,默认为 public。
用 class 声明的类,如果对全部成员不作 private 和public 的声明,默认为 private
类的成员函数 (简称类函数)一般定义在类体中,也可在类外:此时在类中只写声明,在类外定义。
class Student {
public:
void sayHello(); // 👈 只声明,不写函数体
};
// 👇 在类外定义函数体
void Student::sayHello() {
cout << "Hello from outside!" << endl;
}
int main() {
Student s;
s.sayHello();
return 0;
}
类函数与一般函数的区别:类函数的作用域限制在类中
成员函数的存储和this指针
成员函数都隐式的添加了inline关键字,有关内置函数请看:OOP1-C++基础知识 – Skyshin34的博客
当一个类对象被创建时,如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。
为了节省存储空间,C++中,每个对象只存储自己的数据成员,所有对象共享一份成员函数的代码,
this 指针就像是“告诉函数:我当前是为哪个对象服务”。this指向函数的调用者,使得共享的函数代码能够正确访问调用者的数据
比如,当调用成员函数a.volume( )时,编译系统就把对象a的起始地址赋给this指针,于是在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。例如,
volume( )函数要计算height*width*length的值,实际上是执行:(this->height)*( this-> width)* (this-> length)由于当前this指向对象a,因此相当于执行:(a.height)*( a.width)* (a.length)
构造函数
类是一种抽象类型,不是实体 , 不占内存无法容纳数据,所以类的数据成员不能在声明类时初始化的。
构造函数是一种特殊的成员函数。必须与类同名;无返回值;
构造函数是在建立对象时由系统自动执行的,而且只执行一次。不能被用户调用。
如果用户自己没有定义构造函数,系统会自动生成一个函数体为空的构造函数。
C++中还提供了参数初始化表初始化数据成员
3.3 有两个长方柱,其长、宽、高分别为:12,25,30;15,30,21。分别求它们的体积。编写一个程序,在类中定义两个构造函数,其中一个无参数,一个有参数。
//使用构造函数
Box::Box(int h, int w, int len)
{ height=h;
width=w;
length=len;
}
//使用参数初始化表
Box::Box(int h,int w,int len):height(h),width(w),length(len){ }
class Box
{public:
Box() ;
Box(int h, int w, int len):
height(h),width(w),length(len){ }
int volume();
private:
int height;
int width;
int length;
};
Box::Box()
{ height=10;
width=10;
length=10;
}
int Box::volume()
{ return(height*width*length); }
int main()
{ Box box1;
cout<<"The volume of box1 is" <<box1.volume()<<endl;//1000
Box box2(15,30,25) ;
cout<<"The volume of box2 is" <<box2.volume()<<endl;//11250
return 0;
}
使用默认参数的构造函数
编写一个程序,在类中使用含默认参数的构造函数,长、宽、高的默认值均为10,注意不指定参数才会采用默认值
class Box
{ public:
Box(int w=10, int h=10, int len=10);
...
int main()
{
Box box1;
cout<<"The volume of box1 is “<<box1.volume()<<endl;//1000
Box box2(15);
cout<<"The volume of box2 is “<<box2.volume()<<endl;//1500
Box box3(15,30);
cout<<"The volume of box3 is “<<box3.volume()<<endl;//4500
Box box4(15,30,20);
cout<<"The volume of box4 is “<<box4.volume()<<endl;//9000
return 0;
}
声明和定义构造函数时都要指定默认值。在声明构造函数并指定默认值时,形参名可以省略
Box(int =10, int =10, int =10);//声明
X::Box(int = 10,int = 10,int = 10){...} //定义
一个类只能有一个默认构造函数。比如,同时定义以下两个默认构造函数是错误的。
Box( ); //声明一个无参的构造函数
Box(int =10, int =10, int =10);//声明一个指定所有默认参数的构造函数
定义了全部是默认参数的构造函数后,不能再定义重载构造函数。比如,以下定义出现歧义性:
Box(int =10, int =10, int =10);
Box( );
Box(int, int);
Box box1; //调用哪个构造函数?第1个?第2个?
Box box2(15,30); //调用哪个构造函数?第1个?第3个?
转换构造函数
转换构造函数必须有一个或多个参数。有多个参数时,除第一个参数外的其它参数都是默认参数(第一个参数是否默认参数均可)
转换构造函数允许从该参数类型自动转换为当前类类型的对象。转换构造函数是将第一个参数的类型转换为本类类型,它是一种隐式类型转换机制。
#include <iostream>
using namespace std;
class Box {
private:
int length;
public:
// 这是一个转换构造函数:只有一个参数
Box(int l) {
length = l;
cout << "转换构造函数被调用,length = " << length << endl;
}
void print() {
cout << "Box 长度: " << length << endl;
}
};
int main() {
//也可写为Box b1(5);
Box b1 = 5; // 👈 自动调用 Box(int),int -> Box,隐式转换!
b1.print();
return 0;
}
Box b1 = 5;可以这样调用是转换构造函数的特点,普通构造函数不能这样调用
析构函数
执行时间:当对象的生命周期结束时,自动执行析构函数。
和构造函数一样无返回值,必须与类同名但要在类名前面加一“~”符号,此外还规定没有函数参数;
不能被重载,所以一个类中只能有一个析构函数
如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有名字和形式,并不进行任何操作。
何时调用构造函数和析构函数
系统自动调用构造函数和析构函数
构造函数在创建对象时调用,下面列出调用构造函数情况:
- 类声明一个对象将调用一次构造函数。
int main() {
Box b1; // ← 调用一次构造函数
}
- 类声明对象数组(数组大小如为n),将调用n次构造函数。
- 用new运算符创建对象时将调用构造函数。
int main() {
Box* p = new Box(); // ← 调用 1 次构造函数
delete p;
}
出现以下几种情况时,程序会执行析构函数。
- 1.如果在一个函数中定义了一个对象(自动局部对象),当这个函数被调用结束时,或者用delete运算符释放该对象时,在对象释放前自动执行析构函数
- 2.static局部对象在函数调用结束时对象并不释放,因此也不用调用析构函数,此外还有全局对象。他们都只在 main 函数结束或调用 exit函数结束程序时,才调用stactic局部对象的析构函数。
在一般情况下:
调用析构函数的次序正好与调用构造函数的次序相反最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,最后被调用的构造函数,其对应的析构函数最先被调用。
//定义公共的含参构造函数和析构函数,和显示函数,在类外定义整个方法。定义三个私有类属性
//验证析构函数的调用次序
class Student {
public:
Student(int num, string name, char sex);
~Student();
void Display();
private:
int num;
string name;
char sex;
};
int main() {
//如果不需要动态分配,直接创建局部对象(栈上对象)更简单,调用成员函数用 .
Student s(20,"shin",'F');
//用指针接收 new 的结果(动态对象)
Student* s1 = new Student(21, "sky", 'M');
s.Display();
s1->Display();
return 0;
}
Student::Student(int n, string nam, char s)
{
num = n;
name = nam;
sex = s;
cout << "Constructor called." <<num<< endl;
}
Student::~Student() {
cout << "Destructor called." <<num<< endl;
}
void Student::Display()
{
cout << "num:" << num << endl;
cout << "name:" << name << endl;
cout << "sex:" << sex << endl << endl;
}
Constructor called.20
Constructor called.21
num:20
name:shin
sex:F
num:21
name:sky
sex:M
Destructor called.20
类的封装性和信息隐蔽
在声明类时,一般把所有的数据指定为私有的,把需要让外界调用的函数指定为公用的,外界通过公用的函数实现对数据的操作;
公用成员函数作为类的公用接口,与私有实现的分离”形成了信息隐藏。
类声明和成员函数定义的分离
如果一个类只被一个程序使用,可以像之前一样将声明和定义写在同一文件中;
而正规的作法是将类的声明(其中包含成员函数的声明)和函数定义分别放在两个文件中。
//文件: student.h ,头文件,在此文件中进行类的声明
class Student // 类声明
{
public:
void display( ); // 公用成员函数原型声明
private:
int num;
char name[20];
char sex;
};
//文件: student.cpp 在此文件中进行函数的定义
#include <iostream.h>
#include “student.h” // 不要漏写此行,否则编译通不过
void Student :: display( ) // 在类外定义display()函数
{
cout <<“num:”<<num<<endl;
cout <<“name:”<<name<<endl;
cout <<“sex:”<<sex<<endl;
}
//另有主函数的源文件 main.cpp
#include <iostream.h>
#include “student.h” // 将类声明头文件包含进来
int main()
{
Student stud; // 定义对象
stud.display( ); // 执行stud对象的display()函数
return 0;
}
在预编译时会将头文件student.h中的内容取代#include “student.h”行
类库
在实际中,并不是将一个类声明做成一个头文件,而是将若干个常用的功能相近的类声明集中在一起,形成类库。类库有两种:一种是C++编译系统提供的标准类库;一种是自定义类库。用户根据自己的需要
做成的用户类库。
用户只需把类库装入到自己的计算机系统中,并在程序中用#include命令行将有关的类声明的头文件包含到程序中,即可使用这些类和其中的成员函数
类库包括两个组成部分
- 类声明头文件
- 已经过编译的成员函数的定义,它是目标文件
一个现代编译器的主要工作流程如下:源程序(source code)→预处理器(preprocessor)→编译器(compiler)→汇编程序(assembler)→目标程序(object code)→连接器(链接器,Linker)→可执行程序(executables)
类的作用域和类成员的访问(句柄)
在类的作用域内,类的成员(数据和函数)可以被类的所有成员函数直接访问;
在类的作用域之外,public类成员通过对象的句柄(handle)之一而使用
句柄可以是: 对象、对象的引用、对象的指针
每次通过对象的句柄调用成员函数时,编译器会插入一个隐式的this指针
隐藏机制
为了处理不同作用域内之间可能出现重名的实体(变量、函数等),C++中定义了隐藏机制,处理这些重名的情况,并规定:
- 在成员函数中定义的变量属于该函数作用域,只有该函数能访问它们;
- 如果成员函数定义了与类的数据成员同名的另一个变量,在此函数里,函数作用域中的变量将隐藏类的数据成员
- 如果还需要使用被隐藏的数据成员,可以通过在其名前加类名作用域运算符(:: )
PS:成员运算符( . ) 作用域运算符(:: )
注意作用域运算符是 返回值类型 类名::函数名 而不是我之前下意识以为的类名::返回值类型 函数名
void Student::display(); //正确写法
对象(对象引用)访问类成员
对象访问类的成员:通过成员运算符( . ),对象名 .成员名(包括数据成员、成员函数)、
成员必须是公有权限的(public),下面两种情况私有权限(或被保护权限)的成员也可以访问
- 友元函数或友元类里
- 类成员函数定义的本类对象
对象引用其实就是对象,只是取了另外一个对象名而已。所以对象引用访问类成员就和对象访问类成员一样:对象引用名 .成员名;
对象指针访问类的成员
通过对象名后加间接访问运算符( ->),对象名->成员名;
成员也必须是公有权限的(public)
int main()
{
Major m("信息安全"); //创建对象
cout<<m.GetName()<<endl; //对象访问成员函数,用对象名加.运算符
Major& rm = m; //定义对象的引用,rm引用已存在的对象m
cout<<rm.GetName()<<endl; //对象引用访问成员函数,用对象引用名加.运算符
Major* pm1 = &m; //定义对象指针pm1,取对象m的地址给pm1
cout<<pm1->GetName()<<endl; //对象指针访问成员函数,用对象指针名加->运算符
Major* pm2 = new Major("通信工程"); //定义对象指针pm2并用new运算符动态创建对象
cout<<pm2->GetName()<<endl; //对象指针访问成员函数,用对象指针名加->运算符
delete pm2;
return 0;
}
对象的赋值和复制
同类型的变量和对象都可以通过= 赋值。因为C++语言重载了赋值运算符(=),在重载的赋值运算符里实现数据成员逐一赋值。
类中动态分配的数据,默认赋值运算符不会赋值。默认赋值运算符只实现浅拷贝,如要实现深拷贝就需要重载赋值运算符
对象的复制是通过复制构造函数(也称拷贝构造函数)来实现的,对于每个类,编译器都提供了一个默认的复制构造函数,可以将被赋值对象的每个数据成员逐一复制给新对象的相应数据成
Date::Date(const Date& rhs) // 复制构造函数,T( T& ); 或T( const T& );,提倡使用这种声明形式,因为const 使得复制构造函数不可能对被复制的对象进行修改
{ m_nYear = rhs.m_nYear;
m_nMon = rhs.m_nMon;
m_nDay = rhs.m_nDay;
}
int main(){
Date d2(d1); // d2复制d1
}
使用复制构造函数有三种情况:
- 用已有的对象复制创建新的对象
- 函数按值传递和返回对象(因为会创建临时变量)
const 对象
定义:const Date newyear(2022, 1, 1);
const对象的两大限制:
- const 对象从创建后数据成员值就保持不变(即使它们是 public);
- const 对象只能调用const成员函数(即函数用
const
修饰)。
由于 const
对象不能访问会修改自身的成员,所以为了让 const
对象仍然可以读取数据成员,C++ 提供了 const 成员函数:声明和定义时都要加 const
,放在参数列表后:
class Date {
public:
int getYear() const; // ✅ 常对象可调用
void setYear(int y); // ❌ 常对象不可调用
};
int main()
{
const Date d2(2021, 1, 1);
cout <<d2.getYear()<<"-"<<d2.getMon()<<endl;
}
d2调用getmon函数时,编译器将d2(本类成员)的地址赋给this指针,但是d2数据类型为const,所以其指针类型为const Data*/Data const*,但是this的指针地址 是this* const。所以const成员函数的this指针的类型是:const 类*const this
const 成员函数的限制:
- const 成员函数不能修改任何数据成员
- const 成员函数里只能调用const成员函数
构造函数和析构函数因为其特殊性不能声明为const成员函数
mutable 数据成员
const对象的任何数据成员都不能修改,但有时可能有些特例。比如,有某个数据成员总是可以修改的,这时如果不使用const对象又无法保证其它数据成员的安全。
因此,C++语言提供了mutable数据成员这一特性,定义为mutable数据成员就不受const对象和const成员函数的限制,可以任意修改。
全局域 Global Scope
- 定义:在 所有函数和类之外 定义的变量、函数或对象
- 生命周期:整个程序运行期间存在(存储在静态存储区)
- 访问权限:可以被 同一文件 的所有函数访问。通过
extern
声明,可被 其他文件 访问(全局变量跨文件共享)
局部域(Local Scope)
- 定义:在 函数、代码块(
{}
)或类成员函数内 定义的变量或对象。 - 生命周期:从定义点开始,到所在块结束时销毁(存储在栈上)
- 访问权限 – 隐藏机制:仅在定义它的块内可见(如函数内部、
if
/for
块等)。同名局部变量会 隐藏(shadow) 外层变量(如全局变量)。如果还需要在内部使用被隐藏的同名全局变量的数据成员,可以通过在其名前加类名作用域运算符(:: ),全局变量前加作用域运算符来访问
int val = 100;
void badPractice() {
int val = 50; // 遮蔽全局变量
std::cout << val; // 输出: 50(局部变量)
std::cout << ::val; // 输出: 100(全局变量)
}
在类的作用域内,类的成员(数据和函数)可以被类的所有成员函数直接访问;
在类的作用域之外,,public类成员通过对象的句柄(handle)之一:对象,对象的引用,对象的指针来访问。
对象和对象的引用通过对象名 .成员名访问成员,对象的指针通过对象名->成员名访问成员、(每次通过对象的句柄调用成员函数时,编译器会插入一个隐式的this指针)。本质是 简化这个“解引用+访问”的过程的语法糖(syntactic sugar)ptr->member等价于 (*ptr).member
- 为什么
->
不能用于对象:因为->
是为指针设计的语法糖,对象没有指针的解引用步骤。如果强行用obj->member
,编译器会报错(除非类重载了->
运算符,如智能指针)。 - 为什么引用为什么用
.
引用是对象的别名,语法上完全等同于对象本身,因此用.
struct
和class
的成员访问规则完全一致
class Student {
public:
int age;
void study() { cout << "Studying..." << endl; }
};
int main() {
Student s; // 栈上的对象
s.age = 20; // 用 . 访问成员变量
s.study(); // 用 . 调用成员函数
Student& ref = s; // 引用
ref.age = 21; // 用 . 访问引用对象的成员
Student* p = new Student(); // 堆上的对象(指针)
p->age = 20; // 用 -> 访问成员变量
p->study(); // 用 -> 调用成员函数
//如果非要用 . 操作指针,可以先用 * 解引用指针,再使用 .,但代码会冗余,不推荐:
Student* p = new Student();
(*p).age = 20; // 等价于 p->age = 20;
(*p).study(); // 等价于 p->study();
delete p; // 释放内存
return 0;
}
如果成员是私有权限(private)或者被保护权限(protected)无法被访问,但存在两种情况私有权限(或被保护权限)的成员也可以访问:友元函数或友元类里
class MyClass {
private:
int secret = 42; // 私有成员
// 声明友元函数
friend void friendFunction(MyClass& obj);
// 声明友元类
friend class FriendClass;
};
// 友元函数定义
void friendFunction(MyClass& obj) {
cout << "友元函数访问私有成员: " << obj.secret << endl; // 合法
}
// 友元类定义
class FriendClass {
public:
void showSecret(MyClass& obj) {
cout << "友元类访问私有成员: " << obj.secret << endl; // 合法
}
};
类成员函数定义的本类对象
class Student {
private:
int age; // 私有成员
public:
void setAge(int a) { age = a; }
// 成员函数访问其他同类对象的私有成员
void compareAge(const Student& other) {
if (this->age > other.age) { // 直接访问 other.age(合法)
cout << "我比对方年长" << endl;
} else {
cout << "我比对方年轻" << endl;
}
}
};
类作为函数参数
函数参数类型如果是类,和基本数据类型一样,也有三种情况:
- 值传递(类对象)
- 地址传递(对象指针)
- 引用传递(对象引用)
此外const也可修饰对象,所以共有6种函数参数的情况
- 类对象:void func(T t);
- 类对象:void func(const T t);
- 类对象指针:void func(T* pt);
- 类对象指针:void func(const T* pt);
- 类对象引用:void func(T&rt);
- 类对象引用:void func(const T& rt);
编号 | 参数写法 | 是否修改实参 | 说明 |
---|---|---|---|
1 | void func(T t) | ❌ 否 | 所以这两种情实际传递进入函数的是实参的副本;不能修改实参 |
2 | void func(const T t) | ❌ 否 | 对副本也不可能修改 |
3 | void func(T* pt) | ✅ 可以 | 虽然对象指针传递进入函数也是这个对象指针的副本,但因为传递进入函数的是实参的地址,通过对象地址是能够修改实参对象的值;需手动判断空指针 |
4 | void func(const T* pt) | ❌ 否 | 指因为const修饰后是禁止通过地址修改实参的,所以4)的效果是和1)相似。而实际应用中通常更多使用1),因为简洁且安全 |
5 | void func(T& rt) | ✅ 可以 | 实际传递进入函数的就是实参本身(无副本)所以是能够直接修改对象的值。5的效果和3相似,可以修改实参,但使用引用要优于指针 |
6 | void func(const T& rt) | ❌ 否 | 因为const修饰后是禁止修改实参的,所以6)的效果是和1)相似。而实际应用中通常使用6)更优,因为效率更优(因为无副本) |
Static类成员
static成员的声明由关键字static开头
表示了“整个类范围意义上”的信息(即类的所有对象所共享的一个性质,而不是类的某个特定对象的一个属性)
public:类的静态数据和函数成员可以在类外通过”类名::成员名“访问(无需对象),而非 static 成员需要对象,通过 对象.成员
或 对象指针->成员
访问
private:类外不能直接访问,需通过类的public成员函数或友元函数访问
类的对象不存在时,static 成员依然存在,因为static数据成员属于类,每个static数据成员只有一个副本,和任何类对象无关;
成员函数是统一存储,并且通过this指针知道成员函数里访问的数据成员是哪个对象的。
static成员函数也是统一存储,但因为static成员函数可以直接通过类名去访问,所以static成员函数里没有this指针。进一步,因为static成员函数里没有this指针,所以它不能访问非static数据成员。static成员函数里只能使用static数据成员
友元函数和友元类
类之外只能访问public成员,而不能直接访问private成员(或protected成员)。而类之外如要访问private数据成员(或protected数据成员)只有通过public成员函数,但这种机制也会带来一些问题
C++语言提供了“友元”这种方式来实现访问private数据成员(或protected数据成员)的特权
- 友元函数:在类声明中,如函数原型前加关键字friend,这函数就声明为该类的友元函数。这函数可以是全局函数或其它类的成员函数。
- 友元类:在类声明中,如在其它类前加关键字friend,这类就声明为该类的友元类。
友元函数不是类的成员函数
友元关系是不对称的,类A声明类B是友元,并不表示类A也是类B的友元(否则3)点就不成立了)。
友元关系不是传递的,即如果类A是类B的友元,类B是类C的友元,并不能由此推断类A是类C的友元。
友元关系是授予的而不是索取的,类A声明类B是友元,是授予B类访问A类private数据成员的权利,而不是索取访问B类private数据成员的权利。
#include <iostream>
using namespace std;
// 声明B类为A类的友元类
class A {
private:
int privateData;
public:
A(int data) : privateData(data)
{}
// 声明友元类(B类可以访问A的私有成员)
friend class B;
};
class B {
public:
// 访问A类的私有成员
void display(A a) {
cout << "A类的私有数据: " << a.privateData << endl;
}
};
int main() {
A a(100);
B b;
b.display(a); // 输出: A类的私有数据: 100
return 0;
}
组合(composition)
指将对象作为类的成员。创建对象时,构造函数会自动被调用。当包含其它类成员的类创建对象时,其包含的其它类的成员对象也会自动调用构造函数。
- 其它类的成员对象先于包含它们的对象之前调用构造函数
- 如包含多个其它类成员对象,则按它们在包含类里声明的顺序先后调用
- 如其它类有默认构造函数,可以不在参数初始化列表显式调用
#include <iostream>
using namespace std;
class B {
public:
B() {
cout << "B 的构造函数被调用" << endl;
}
};
class A {
private:
B b; // B 是 A 的成员对象
public:
A() {
cout << "A 的构造函数被调用" << endl;
}
};
int main() {
A a; // 创建 A 的对象
return 0;
}
//输出结果
//B 的构造函数被调用
//A 的构造函数被调用