一,类的定义
cpp
class classname
{
//类体由成员函数和成员变量组成
};
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分 号不能省略。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
二,类的访问限定符
- 访问限定符有三种分别为 public private protected
- public:类内类外都可以直接访问
- private:只有类内成员可以访问,类外成员不可以访问
- protected:只有类内成员可以访问,类外成员不可以访问
- C++中如果没有明确声明,类中的所有成员和函数都是私有的,即private
- 访问限定符的作用范围为从该限定符开始一直到下一个限定符,或者类的结束
三,类中的this指针
this指针的相关特性
- this指针只作用于类的内部
- 类中的非静态成员函数其参数列表中都有一个this指针,this指针的传递由编译器帮组我们完成,不需要用户主动传参
- this指针指向当前对象,被const所修饰,不可被修改
- 当我们在类的内部调用成员函数或者成员变量时(非静态),是以this->(成员函数或者成员变量),但是this->一般可以省略
关于this指针的小练习
cpp
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
- 第一个正常运行,因为void Print()内部没有调用成员函数和成员变量,所以就没有用到this指针,所以没有报错能正常运行
- 第二个报错,因为void Print()内部用到了成员变量,也就用到了成员变量,所以会报错
四,类的6个默认成员函数
构造函数
- 函数名与类名相同,创建对象时由编译器自动调用,没有返回值
- 主要任务不是开辟空间创建对象,而是用于初始化成员函数
- 构造函数可以重载
- 如果我们没有显示的在类中写构造函数,编译器会自动的帮我们生成一个无参的默认构造函数,该默认构造函数在构建对象时会调用成员变量的构造函数(只对自定义类型生效)
- :C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值。
析构函数
- 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
- 析构函数名是在类名前加上字符 ~,无参数无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 如果我们没有显示的在类中写析构函数,编译器会自动的帮我们生成一个析构函数数,该析构函数在对象销毁时会调用成员变量的析构函数
拷贝构造函数
- 拷贝构造函数是构造函数的一个重载形式,函数名与类名相同,没有返回值
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
参数不是类类型对象的引用错误示例如下:
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date& d)
// 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
错误原因如下:
直接使用浅拷贝错误示例如下:
cpp
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
运行结果如下:
原因:程序退出时s2,s1都需要调用析构函数对内存进行释放,s2先调用析构函数对所申请的空间进行释放,这是s1并不知道内存已经被释放了,又因为浅拷贝指针指向的是同一块空间,对已经释放过的空间再次进行释放,必然会报错
赋值运算符重载
运算符重载
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
- . * :: sizeof ?: . 注意以上5个运算符不能重载
赋值运算符重载
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 返回*this :要复合连续赋值的含义
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
- 内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。
- 如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必 须要实现,原因与上述拷贝构造相同
- 赋值运算符只能重载成类的成员函数不能重载成全局函数,示例如下:
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
}
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
// 编译失败:
// error C2801: "operator ="必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
前置++和后置++重载
- 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
前置++如下:
cpp
Date& operator++()
{
_day += 1;
return *this;
}
后置++如下:
cpp
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
五,const成员
- 将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改(上文中提到的this被const修饰,是指针自身不能被修改)。
六,再谈构造函数
- 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始 化一次,而构造函数体内可以多次赋值。
- 所以我们引进一个新的概念初始化列表
初始化列表
- 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式,表现形式如下:
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后 次序无关
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。
- 类中包含以下成员,必须放在初始化列表位置进行初始化
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
explicit关键字
- 构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。
- 单参构造函数,没有使用explicit修饰,具有类型转换作用
- explicit修饰构造函数,禁止类型转换
七,static成员
static成员概念
- 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
计算程序中创建出了多少个类对象
cpp
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}
答案:是一个
static成员性质
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
- 静态成员函数不可以调用非静态成员函数
- 非静态成员函数可以调用类的静态成员函数
八,友元
友元函数
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以 友元不宜多用
友元分为:友元函数和友元类
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性(A为B的友元类,A可以访问B中的所有私有成员函数和变量,但是B不能访问A中的私有成员函数和成员)
- 友元关系不能传递(如果C是B的友元, B是A的友元,则不能说明C是A的友元)
- 友元关系不能继承,在继承位置再给大家详细介绍
九,内部类
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越 的访问权限
- 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元
- 内部类可以定义在外部类的public、protected、private都是可以的
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
- sizeof(外部类)=外部类,和内部类没有任何关系
十,匿名对象
- 假设有一个类的类名为A,定义对象时不能 A+对象名+(),这样编译器无法识别是函数声明还是定义对象
- 可以A+()定义对象,作用域为该行