文章目录
这篇文章大概包含了类中所有在初阶时需要学习了解的内容。
其内容仅对this未进行介绍。
类
1.类的概念
class Date
{
public:
int _c;
void Printf()
{
cout<<_c<<_a<<endl;
}
private:
int _a;
};
上述类基本包含一个类的基本元素。
首先,在理解上,类可以看作结构体的升级。
我们不仅可以在类中声明数据,还可以声明函数。并且还对其内部空间进行划分,保证数据的安全性。
2.类的访问限定符
class Date
{
public:
int _c;
void Printf()
{
cout<<_c<<_a<<endl;
}
private:
int _a;
};
类在定义时,便创造了一个新的作用域,类域。
类的所有成员都在类域中。
访问限定符便对类域的空间进行了划分,访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
public为公有。
我们可以在类外使用公有数值或调用公有函数
如:
Date x;
x._c=3;
x.Printf();
private为私有。
我们不能在类外使用它们。
除了上面两种,还有一种为protected(保护),其功能与private类似,在此不做分析。
当类中并无限定符时,则默认为公有。
在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Date
{
public:
int _c;
void Printf();
private:
int _a;
};
void Date::Printf()
{
cout<<_c<<_a<<endl;
}
3.类中成员的存储
1.存储方式
对于类中的函数,若每创建一个类对象同时存储函数与变量,那么会造成极大的空间浪费。
所以对于类中的成员函数,其统一存储在公共代码区,而变量则存储在对象中。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
2.存储对齐规则
第一个成员在与结构体偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8。
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
4.类的6大默认成员函数
1.构造函数与析构函数
1.构造函数与析构函数的概念
class Date
{
public:
private:
int _a;
};
int main()
{
Date s1;
return 0;
}
当我们创造一个对象时,对象中的变量需要初始化,此时用于初始化变量的函数就是构造函数
而当main函数结束,该对象生命周期结束,此时对象内变量需要被销毁,用于销毁它们的函数就是析构函数。
2.构造函数与析构函数的特征与调用
1.构造函数与析构函数无返回值
2.构造函数函数名为类名,析构函数函数名为:~类名
class Date
{
public:
Date(int a=2)//构造函数
{
_a = a;
}
~Date()//析构函数
{
}
private:
int _a;
};
int main()
{
Date s1(3);
Date s2;
return 0;
}
对于构造函数,其在创建对象时调用,形式如s1
而当构造函数为全缺省时,也可以像s2一样使用
3.系统默认生成的构造函数与析构函数
构造函数与析构函数作为类的默认成员函数,若我们不写,则会由系统自动生成。
生成的构造函数对于类中的内置类型(变量)不做处理,而对于自定义(类型变量)会去调用它的构造函数。
生成的析构函数对于类中的内置类型(变量)自动销毁,而对于自定义(类型变量)会去调用它的析构函数。
而它们对于类中需要在堆上申请空间的变量,要么不做处理,要么无法销毁。
4.什么时候自己写
当类中含有需要在堆上申请空间的成员变量时,我们才需要自己写
2.拷贝构造函数
1.拷贝构造函数的概念
class Date
{
public:
Date(int a=2)//构造函数
{
_a = a;
}
private:
int _a;
};
int main()
{
Date s1(3);
Date s2(s1);
return 0;
}
当我们创造一个对象时,我们希望用另一个对象的变量值来初始化它,此时所需的函数为拷贝构造。
2.拷贝构造函数的特征与调用
调用如s2的创建。
拷贝构造实际上是构造函数的重载。
无返回值,函数名与类名相同,形参只有一个,为一个对象的引用
class Date
{
public:
Date(int a=2)//构造函数
{
_a = a;
}
Date(const Date&a)//拷贝构造函数
{
_a = a._a;
}
private:
int _a;
};
为什么其形参必须为引用而非值传递呢?
其原因在于值传递的本质就是拷贝构造。
对于上图的s2的初始化而言,我们需要用s1拷贝构造s2。
而进行拷贝构造时,我们需要用s1拷贝构造a,此时再一次调用拷贝构造函数。
其会发生无穷递归,所以其形参只能是对象的引用。
3.系统默认生成的拷贝构造函数
拷贝构造作为类的默认成员函数,若我们不写,则会由系统自动生成。
生成的拷贝构造函数对于类中的内置类型(变量)会按照字节方式直接拷贝的,而对于自定义(类型变量)会去调用它的构造函数。
4.什么时候自己写
对于拷贝构造而言,系统生成的看似很美好,但还是有问题。
其无法进行深拷贝。若成员变量s1在堆上申请了空间,那么s2中的成员变量会与s1中的一模一样,导致s1与s2共用同一变量。其不仅仅是在使用s1,s2会出现问题,而且在调用它们的析构函数时,由于会调用2次,所以第二次会找不到那个变量,从而导致程序崩溃。
3.赋值重载函数
与前三个函数学习不同,在学习赋值重载函数时,我们要先学习运算符重载,请先看完5.运算符重载,再来看此章节
1.赋值重载的实现
在完成运算符重载的学习之后,我们现在可以直接写出类的赋值重载:
class Date
{
public:
Date(int year = 1,int month =1,int day =1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
const Date & operator = (const Date& s2)
{
_year = s2._year;
_month = s2._month;
_day= s2._day;
cout << this << endl;
return *this;
}
private:
int _year;
int _month;
int _day;
};
我们可以注意到,其返回值用const修饰了,对于我们不进行修改原数的运算,我们都应用const修饰
赋值运算符只能在类内部重载,不能在类外
3.系统默认生成的赋值重载
赋值重载作为类的默认成员函数,若我们不写,则会由系统自动生成。
生成的赋值重载对于类中的内置类型(变量)会直接复制,而对于自定义(类型变量)会去调用它的赋值重载函数。
4.什么时候自己写
对于赋值重载而言,系统生成的看似很美好,但还是有问题。
其无法进行深拷贝。若成员变量s1,s2都在堆上申请了空间,那么s2中的成员变量会被赋值为与s1中的一模一样,导致s1与s2共用同一变量。
其不仅仅是在使用s1,s2会出现问题,而且在调用它们的析构函数时,由于会调用2次,所以第二次会找不到那个变量,从而导致程序崩溃。
而且,我们想一想,s2与s1一样了,那么原先s2申请的空间无法析构,还造成了内存泄漏。
4.取地址重载
1.实现
由于其本身很少需要我们自己写,我们简单的看一下。
class Date
{
public:
Date* operator&()//正常对象
{
return this;
}
const Date* operator&()const//const修饰的对象
{
return this;
}
private:
int _year;
int _month;
int _day;
};
第一个取地址重载大家很容易理解,而第二个是针对于const修饰的对象的,而函数后的const我们现在来讲解。
2.const修饰的函数
const Date* operator&()
{
return this;
}
const Date s2;
我们要想取到s2的地址。那么上面的函数是无法实现的。
因为this指针本身是Date * const的类型,而s2是const Date类型,在传递时,会发生权限放大。
由此,我们引入const,在成员函数后加const,用于修饰this指针。
对于不需要修改自身的运算,都应加上const来修饰this指针,可以避免很多权限放大的问题
5.运算符重载
1.运算符重载的概念
运算符重载,其理解类似于函数重载。
我们要实现的是使一个自定义类其在操作时,可以像一个内置类型一样,流畅的使用各种运算符。
2.运算符重载的实现
class Date
{
public:
Date(int year = 1,int month =1,int day =1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator == (const Date & s1, const Date & s2)
{
return (s1._year == s2._year) && (s1._month == s2._month) && (s1._day == s2._day);
}
我们规定在 operator后加运算符。
在上面完成了一个==的重载。
但是我们发现若在类外完成函数的定义,我们很难访问到私有变量,而若为了完成重载而将变量公有,便是本末倒置。
所以我们将该运算符重载写在类中,这样便可以实现对类中私有成员的访问。此时,对于多操作数的运算符,我们规定,左边第一个操作数为this指针。所以我们可以将上函数改写为:
class Date
{
public:
Date(int year = 1,int month =1,int day =1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
bool operator == (const Date& s2)
{
return (_year == s2._year) && (_month == s2._month) && (_day == s2._day);
}
//private:
int _year;
int _month;
int _day;
};
而对于重载的使用则有2种形式:
cout << (s1 == s2) << endl;
cout << (s1.operator==(s2)) << endl;
注意
1.对于运算符重载,应着重关注其返回值。
多数运算符都要求链式运算,如:s1=s2=s3,其先进行s2=s3,再将s2作为式子的返回值
2. .* :: sizeof ? : . 注意以上5个运算符不能重载
3.不能通过连接其他符号来创建新的操作符:比如operator@
6.流运算符重载
在学习运算符重载后,我们是否也能用cout,cin直接来进行类对象的输入输出呢?
首先我们明确一下,cout是一个类的对象,类名为ostream
class Date
{
public:
ostream& operator<<(ostream& out)
{
out << _year << _month << _day << endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
完成上述代码后,我们发现:cout<<s1;无法通过编译。
因为对于:<<来说,其左操作数为*this,所以若要使用,如下
s1<<cout;//十分的别扭
而当我们把它写在类外时,又无法访问私有,所以此时引入一个新概念,友元。
class Date
{
friend ostream& operator<<(ostream& out, Date& s1);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out,Date &s1)
{
out << s1._year << s1._month << s1._day << endl;
return out;
}
7.友元
友元函数与友元类
在流重载中,我们使用了友元函数。即将函数声明写在类中,并在前加friend,此时,我们在该函数中可以访问类的私有对象,而友元类的实现与之相似。
class A
{
friend class B;
private:
int _a = 5;
};
class B
{
public:
void Printf()
{
cout << _b << _aa._a << endl;
}
private:
int _b = 3;
A _aa;
};
如上,B中函数可以访问A中变量。
1.友元是单向的。B是A的友元,而A不是B的友元
2.友元函数可访问类的私有和保护成员,但不是类的成员函数
3.一个函数可以是多个类的友元
4.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
5.友元函数并非类的成员函数
8.static
1.引入
当我们想统计一个类直接构造了多少个对象应该怎么做呢?
很简单,我们只需要定义一个全局变量即可。
int N=0;
class Date
{
public:
Date(int year = 1,int month =1,int day =1)//构造函数
{
N++;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
但是,这样很不安全,谁都可以访问,修改你的全局变量。而若是在类中定义一个变量,它又无法统计。因此,我们引入static。
2.静态变量与静态函数
class Date
{
public:
Date(int year = 1,int month =1,int day =1)//构造函数
{
N++;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
static int N;
};
int Date::N=0;
对于static变量,我们应该在类外进行定义与初始化。
而它存于静态区,为了保证安全,我们将它放在私有,可以通过成员函数来访问它。
class Date
{
public:
static void P()
{
cout<<N<<endl;
}
Date(int year = 1,int month =1,int day =1)//构造函数
{
N++;
_year = year;
_month = month;
_day = day;
}
static
private:
int _year;
int _month;
int _day;
static int N;
};
int Date::N=0;
用static修饰的是静态成员函数,其存储位置与其他成员函数相同。
特点:
1.没有this指针,无法访问类中变量。
2.可以访问静态成员。
9.内部类
class A
{
public:
private:
class B
{
private:
int _b=3;
};
int _a=0;
}
内部类实际上就是定义在类中的类,类种类。
内部类的特点:
1.内部类天生就是外部类的友元
2.内部类的访问受外部类的访问限定符限制
3.内部类看似在外部类中,实际上除访问限制外,它与一个正常的友元类作用相同
4.内部类不影响外部类的大小
5.内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
10.explict
class A
{
private:
int _a=0;
};
int main()
{
A a=1;
return 0;
}
我们发现上面a的创造既不是构造,也不是赋值。
它实际上是隐式类型转换,编译器先用1构造一个临时变量,再用临时变量拷贝构造a。
而若A中有多个变量,则只有当其全缺省或半缺省(只有第一个参数未缺省)才触发上图的隐式类型转换。
class A
{
public:
A(int a,int b,int c)
{
_a=a;
_b=b;
_c=c;
}
private:
int _a;
int _b;
int _c;
};
int main()
{
A a={1,2,3};
return 0;
}
对于多参的隐式类型转换,实际上它很像我们定义数组,也体现了c++是c的++。
11.初始化列表
1.引入
class A
{
public:
A(int a=0)
{
_a=a;
}
private:
const int _a;
};
我们发现这个类报错了。因为它的成员变量被const修饰了,而const修饰的变量只能在定义时赋值。由此,引入初始化列表。
2.应用
规定其在构造函数与函数体之间,以 : 开始,以 ; 换行
class A
{
public:
A(int a=0)
:_a(a)
{
}
private:
const int _a;
};
就像上面说的,const只能在初始化时赋值。
在C++中,我们创造对象时,规定其成员变量在初始化列表定义。
回想变量的缺省值,当我们调用构造函数时,如果有缺省值,便用缺省值为它初始化。
无论何时构造函数都会自动使用初始化列表,若我们在构造函数中赋值,它也是先使用初始化列表,所以为了简化最好在初始化列表为变量赋值。
3. 注意
一、
有三种变量是必须使用初始化列表初始化的。
1.const修饰的
上面已经说了。
2.引用
对于引用,它也是必须在定义时赋值的变量。
3.没有默认构造函数的自定义类型
由于我们会先在初始化列表定义它,而它又没有默认构造,所以如果不在初始化列表赋值,它就会报错。
二、
初始化列表的顺序是成员变量声明的顺序。(很怪的一点,我们需要记住)
12.匿名对象
class A
{
public:
A(int a=0)
:_a(a);
{
}
private:
int _a;
};
int main()
{
A a1;
A ();
return 0;
}
对于上面的a1,我们知道它的生命周期是main函数。
对于A(),它也是一个对象,只不过没有名字。它的生命周期只有一行。
使用场景:
在我们想调用类的函数,又不想创建对象时。
实际上,在我们一系列偷懒的时候都有用。