一、类的引入
1、结构体的升级
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct Stack
{
//成员函数
void Init()//不用穿参数,可以直接访问
{
a = nullptr;
top = capacity = 0;
}
void Push(int x)
{}
//成员变量
int* a;
int top;
int capacity;
};
2、类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
(1)含义
①class为定义类的关键字
②ClassName为类的名字
③{}中为类的主体
注意类定义结束时后面分号不能省略,和结构体的类似。
(2)类体中内容称为类的成员;
类中的变量称为类的属性或成员变量;
类中的函数称为类的方法或者 成员函数。
(3)当类的声明放在.h的文件中,成员函数定义放在.cpp文件中,需要在成员函数名前加上类名
,h文件中:
class Stack
{
public:
//成员函数
void Init();//不用穿参数,可以直接访问
void Push(int x);
int Top();
private://修饰从这里开始到下一恶搞访问限定符或者到结束
//成员变量,一般成员变量都是私有
//类里面不受访问限定符的限制,访问限定符限制的是外面的访问
int* a;
int top;
int capacity;
};
.cpp文件中:
void Stack::Push(int x)
{
if (top == capacity)
{
size_t newcapacity = calloc == 0 ? 4 : capacity * 2;
a = (int*)realloc(a, sizeof(int) * newcapacity);
capacity = newcapacity;
}
a[top++] = x;
}
3、类的访问限定符及封装
(1)C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用。
(2)访问限定符
①public:修饰的成员在类外可以直接被访问。
②protected:修饰的成员在类外不能直接被访问。
③private:修饰的成员在类外不能直接被访问。
④访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现或者结束时为止。
⑤class的默认访问权限为private,struct为public(因为struct要兼容C)。
4、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
类的成员变量可以加上“_”进行修饰,以作区分。
class Date
{
public:
private:
int _year;//这是一个声明
int _mouth;
int _day;
};
5、类对象的存储方式
(1)只保存成员变量,成员函数存放在公共代码段。
(2)一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象,表示这个对象存在,不是为了存储数据,并且也无法进行访问。
6、this指针
(1)概念:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。这样就可以解决不同对象的函数体区分。
class Date
{
public:
void Init(int year,int month,int day)
{
_year = year;
}
//编译器修改为:
/*void Init(Date* thie,int year, int mouth, int day)
{
this->_year = year;
this->_mouth = mouth;
}*/
private:
int _year;//这是一个声明
int _month;
int _day;
};
(2)this指针的特性
①this指针的类型:类类型* const,即this指针不能被修改。
②this在实参和形参的位置不能显示的写,但是在类里面可以显示的用,this在C++中是作为一种关键字。
③this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针,this指针一般在栈区里。
④this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递
7、类的实例化
(1)概念:用类类型创建对象的过程是类的实例化。
(2)一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
二、类的默认成员函数
概念:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数
1、概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。构造函数的主要任务并不是开空间创建对象,而是初始化对象。
2、特性
(1)函数名和类名相同
(2)没有返回值:这里不需要写void。
(3)对象实例化时编译器自动调用对应的构造函数
(4)构造函数可以重载:本质是可以写多个构造函数,提供多种初始化方式。
3、构造函数的使用
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数}
若通过无参数构造函数创建对象时,对象后面不需要括号。
原因:这样不清楚这是否是一个函数声明。
4、编译器自动生成的默认构造函数特点
默认构造函数:不需要参数就可以使用的构造函数。
(1)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
(2)对于内置类型(语言原生的关键字成员的类型)的成员编译器不会进行处理。但是有些编译器会处理成0,在C++11后支持声明时给缺省值。
(3)对于自定义类型的成员编译器才会进行处理,这个时候回去调用这个成员的默认构造函数,如果没有默认构造函数,就会报错。
(4)无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
总结:一般情况下都需要自己写构造函数,决定初始化方式,除了成员变量全是自定义类型的时候可以考虑不写构造函数,因为这里编译器会去调用自定义类型的构造函数。构造函数最方便的就是防止自己忘记,编译器会自动调用生成。
析构函数
1、概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的,栈帧销毁,对象本身也就销毁。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2、特性
(1)析构函数名是在类名前加上字符 ~。
(2)无参数无返回值类型:不能构成函数重载。
(3)一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载。
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。
3、自己写析构函数的情况
有malloc和fopen等动态申请和显式申请的需要自己写析构函数释放。
例如自己实现栈时需要写析构函数
~Stack()
{
cout << "~Stack()" << endl;
free(a);
a = nullptr;
top = capacity = 0;
}
4、默认析构函数特点
(1)内置类型成员不做处理。
(2)自定义类型成员会调用这个成员的析构函数。
5、构造和析构顺序
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
(1)构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构。但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
(2)全局对象先于局部对象进行构造
(3)局部对象按照出现的顺序进行构造,无论是否为static。所以构造的顺序为 c a b d
(4)析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构。因此析构顺序为B A D C
拷贝构造函数
1、概念
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
class Date
{
public:
Date(int year = 1, int month = 1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
2、特征
(1)拷贝构造函数是构造函数的一个重载形式。
(2)拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
(3)若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝,对子在堆开辟的空间,浅拷贝可能导致两个值指向同一个空间,在析构的时候会有析构两次的问题。因此类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。在写的时候可以加上const防止误改。
(4)在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。
3、使用场景
(1)使用已存在对象创建新对象。
(2)函数参数类型为类类型对象。
(3)函数返回值类型为类类型对象。
取地址及const取地址操作符重载
1、二者为默认成员函数,会自动生成
2、取地址重载日常自动生成的就可以,不想被取到有效地址的时候就不行,需要自己写。
三、赋值重载
(一)运算符重载
1、概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
(1)函数名字:关键字operator后面接需要重载的运算符符号。
(2)函数原型:返回值类型 operator操作符(参数列表).
2、注意
(1)不能通过连接其他符号来创建新的操作符:比如operator@ .
(2)重载操作符必须有一个类类型参数。
(3)用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
(4)不能改变操作符的操作数个数,一个操作符是几个操作数,重载的时候就有几个参数。作为类成员函数重载时,其形参看起来比操作数数目1,因为成员函数的第一个参数为隐藏的this。
(5) .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
(6)函数左边是第一个操作数,右边是第二个操作数,这个不能改变。
(二)赋值运算符重载
1、赋值运算符重载格式
(1)参数类型:const T&,传递引用可以提高传参效率
(2)返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
(3)检测是否自己给自己赋值
(4)返回*this :要复合连续赋值的含义
2、赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
3、用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
4、如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必 须要实现。
5、拷贝构造和赋值的区别:赋值是两个已经存在的对象进行拷贝,拷贝构造是一个已经存在的对象去初始化另一个要创建的对象。
Date& Date::operator=(const Date& d)
{
if (this != &d)//二者相等的话就不进行赋值操作
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
四、const对象
1、概念
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
2、const对象的使用
下面两个函数构成重载,编译器会认为const Date*和Date*的类型不同,构成重载。
(1)二者在使用时编译器会走最匹配的,对于const的变量如d1会走有const修饰的函数,第二个d2可以走没有const的版本,因为权限可以缩小,有非const版本就直接走非const的版本。
(2)函数读写功能分开的时候会需要这么写
(3)const和非const都可以的时候,写成const更好,但是不能无脑加,只读函数可以加const(内部不涉及修改成员),加上后const对象和非const对象都可以调用。声明和定义分离,两边都要加const
五、explicit关键字
1、自定义类型的隐式转换
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。当有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,也具有类型转换作用。
2、关键字功能
用explicit修饰构造函数,将会禁止构造函数的隐式转换。
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2;
}
(1)对于单参数构造函数的隐式类型转换,编译器会用2调用A的构造函数生成一个临时对象,这个临时对象会有常性,再用这个对象去拷贝构造aa2,连续的构造和拷贝构造编译器会进行优化,优化为用2直接构造。因此上述代码的运行结果为:
(2)多个参数时,C++98不支持,C++11支持
class B
{
public:
B(int i, int j)
{
cout << "b(int i int j)" << endl;
}
private:
int _b1;
int _b2;
};
int main()
{
B bb1(1, 2);
B bb2{ 1,2 };
}
运行结果:
六、匿名对象
1、匿名对象特点
生命周期只在这一行,其他人也用不了。有名对象生命周期在当前局部域。
2、功能:
对象只用一次时,可以简化代码,将两行的事情简化为一行
int main()
{
A(1, 2);//这是一个匿名对象
return 0;
}
七、static静态成员
1、概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。
2、静态成员
(1)静态成员变量一定要在类外进行初始化,初始化时不添加static关键字,类中只是声明。
(2)静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
(3)静态成员不能走缺省值,静态成员不走初始化列表,初始化列表是某个对象成员定义的地方,然而静态成员属于全体,存储在静态区。因此不能给缺省值。
(4)类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。
class A
{
public:
A()
{}
A(const A&t)
{}
private:
static int n;//统计累计创造了多少个对象
static int m;//统计正在使用多少个对象
};
//相当于声明和定义分离
int A::n = 0;
int A::m = 0;
3、静态成员函数
(1)特点:没有this指针,以前用对象传,现在只用突破类域即可。
(2)静态成员函数不能访问非静态成员,只能访问静态,因为没有this指针,非静态成员需要this指针访问
(3)静态成员函数可以用类名访问
class A
{
public:
A(int i, int j)
{
m = i;
n = j;
}
static void Print()
{
cout << m << n << endl;
}
private:
static int n;
static int m;
};
int A::n = 0;
int A::m = 0;
int main()
{
A(1, 2);
A::Print();
return 0;
}
运行结果:
八、友元
1、概念
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以 友元不宜多用
2、友元函数
(1)概念
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
class Date
{
//友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);//这里要修改内部的值
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
(2)说明
①友元函数可访问类的私有和保护成员,但不是类的成员函数
②友元函数不能用const修饰
③友元函数可以在类定义的任何地方声明,不受类访问限定符限制
④一个函数可以是多个类的友元函数
⑤友元函数的调用与普通函数的调用原理相同
3、友元类
(1)概念
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
(2)特点
①友元关系是单向的,不具有交换性。 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
②友元关系不能传递 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
③友元关系不能继承。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
九、内部类
1、概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。
2、特性:
(1)内部类可以定义在外部类的public、protected、private都是可以的。
(2)注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
(3)sizeof(外部类)=外部类,和内部类没有任何关系。内部类是共有的
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;