类是C++的一个最重要的特性。类与结构体的扩充形式十分相似,类声明中的内容包括数据和函数,分别称为数据成员和成员函数。
1.类与对象的初步认识
a.类是抽象的对象,而对象是类的具体实例。
类是抽象的,不占用内存;而对象是具体的,占用内存空间。
b.面向过程与面向对象
C语言是面向过程的,关注的是过程中的数据与方法
C++是面向对象的,关注的是对象的属性与功能
关于面向对象的理解
面向对象的三大特性:封装、继承、多态
面向对象就是一种常见的程序结构设计方法。
面向对象思想的基础是将相关的数据和方法放在一起,组合成一种新的符合数据类型,然后使用新创建的复合数据类型作为项目的基础。面向对象是一个抽象的概念,它相对面向过程而言。面向过程,强调的是功能行为,一种过程,先做什么,再做什么。
而面向对象是将功能封装到对象里,强调的是具备某功能的对象。
类的引入
在C++中,结构体内不仅可以定义变量,也可以定义函数
struct Student
{
char _name[20];
char _gender[3];
int _age;
void SetStudentInfo(const* name,const char* gender, int age)
{
strcpy(_name,name);
strcpy(_gender,gender);
_age=age;
}
};
int main()
{
Student s;
s.SetStudentInfo("Peter","男",18);
return 0;
}
上面结构体的定义,在C++中更喜欢用class来代替
类的定义
class Date
{
//类体:由成员函数和成员变量组成
}: //一定要注意后面的分号
class为定义类的关键字,Date为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员;类中的数据称为类的属性或者类的成员变量;类中的函数称为类的方法或者类的成员函数
提到了类的定义,我们可以来了解一下类的三种访问限定符
1.public成员可以从类外部直接访问,private/protected成员不能从类外部直接访问。
2.每个限定符在类体中可使用多次,它的作用域是从该限定符出现开始到下一个限定符之前或类体结束前。
3.类体中如果没有定义限定符,则默认为私有的。
4.类的访问限定符体现了面向对象的封装性。
定义一个简单的类,示例如下:
在C语言中,striuct是一个结构体,在C++中,struct可以当作是一个结构体,也可以当作是类。它跟class的区别就是,class在不标明访问限定符的情况下,它一般被认为是私有的,而struct默认是公有的。
下面举一个struct在C++中作为类的代码示例:
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;
};
int main()
{
ListNode* cur;
return 0;
}
因为这个只是知识点整理,所以这里就不介绍太多。下面我们了解一下类的作用域。
在C++中,类有四个作用域:局部域、全局域、类域、名字空间域
1.每个类都定义了自己的作用域,类的成员(成员函数/成员变量)都在类的作用域内,成员函数内可任意访问成员变量和其它成员函数。
2.对象可以通过直接访问公有成员,指向对象的指针通过->也可以直接访问对象的公有成员。
3.在类体外定义成员,需要使用::作用域解析符指名成员属于哪个类域。
类实例化对象:
图片示例:
1.类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间存储类成员变量。
3.做个比方。实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样的类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
类实例化对象代码示例:
class Person
{
public:
void showInfo()
{
cout<<_name<<"-"<<_sex<<"-"<<_age<<endl;
}
public:
char* _name; //名字
char* _sex; //性别
int _age; //年龄
};
void Test()
{
Person man;
man._name="jack";
man._age=10;
man._sex="男";
man.showInfo();
}
类对象存储模型
C++的类定义出来的对象大小为类中所有成员变量的大小之和,但要注意不包括成员函数。因为比如说,一个Person函数定义了p1,p2,p3,p4四个对象会存储不同的人,不同的信息。而且,当它们去调函数的时候调的也是同一个函数,由此说明这个函数不需要存到对象中,可以随便存到公共区域,要调的时候都来找这个函数就可以了。当然,这里也要遵循内存对齐原则。
内存对齐是为了提高效率,因为CPU读数据按CPU字长,然后按整数倍读,不对齐的话可能会导致一个变量的字节放在两个部分里面可能要读两次。
那为什么存在内存对齐呢?我们可以用C语言中的知识来进行解释
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则就抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问需要访问一次。
如图:
结构体内存对齐规则:
1.第一个成员与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。(对齐数=编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8 gcc中的默认值为4)
3.结构体总大小为最大对齐数(每个成员变量除了第一个成员都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们来看一道题:
struct s1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n",sizeof(struct s1));
return 0;
}
这道题当我们不了解内存对齐的时候,会不会觉得答案是6,但是答案肯定不是6,答案是12,解释如下:
再看另一个例子:
struct s2
{
char c1;//1字节 对齐数为1
char c2;//1字节 对齐数为
int i; //4字节 对齐数为4
};
int main()
{
printf("%d\n",sizeof(struct s2));
return 0;
}
结果为8. 1+1+2+4=8,8是最大对齐数4的倍数。
struct s3
{
double d;//8字节 对齐数为8
char c; //1字节 对齐数为1
int i; //4字节 对齐数为4
};
int main()
{
printf("%d\n",sizeof(struct s3));//16
return 0;
}
//结果为16,8+1+3+4=16,16是最大对齐数4的倍数
struct s3
{
double c;//8字节 对齐数为8
char c; //1字节 对齐数为1
int i; //4字节 对齐数为4
};
struct s4
{
char c; //1字节 对齐数为1
struct s3 s;//16字节 对齐数为8
double d; //8字节 对齐数为4
};
int main()
{
printf("%d\n",sizeof(struct s4));
return 0;
}
//结果为32.1+7+16+8=32,32是最大对齐数8的倍数
类中一共有六个默认成员函数,分别是构造函数、拷贝构造函数、析构函数、赋值操作符重载、取地址操作符重载、const修饰的取地址操作符重载。这里就重点介绍一下前四个。
一.构造函数
成员变量为私有的,要对它们进行初始化,必须用一个公有成员函数来进行。同时这个函数应该有且仅在定义对象时自动执行一次,这时调用的函数称为构造函数。
构造函数是特殊的成员函数,其特征如下:
1.函数名与类名相同。
2.无返回值。
3.对象构造(对象实例化)时自动调用对应的构造函数。
4.构造函数可以重载。
5.构造函数可以在类中定义,也可以在类外定义。
6.如果类定义中没有给出构造函数,则C++编译器自动产生一个缺省的构造函数,但只要我们定义了一个构造函数,系统就不会自动生成缺省的构造函数。
7.无参的构造函数和全缺省值的构造函数都认为是缺省构造函数,并且缺省的构造函数只能有一个。
无参的构造函数以及带参的构造函数代码示例:
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 TestDate1()
{
Date d1; //调用无参构造函数
Date d2(2018,1,1); //调用带参的构造函数
Date d3(); //注意这里没有调用d3的构造函数定义出d3
}
带缺省参数的构造函数 代码示例:
class Date
{
public:
//3.缺省参数的构造函数
Date (int year=2018,int month=2,int day=15)
{
_year=year;
_month=month;
_day=day;
}
//4.半缺省参数的构造函数(不常用)
Date(int year,int month=9)
{
_year=year;
_month=month;
_day=1;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;
Date d2(2018,9,12);
}
//注意:若缺省参数声明和定义分离,则可以在声明或定义中给默认参数
二.拷贝构造函数
创建对象时使用同类对象来进行初始化,这时所用的构造函数称为拷贝构造函数,拷贝构造函数是特殊的构造对象。
特征:
1.拷贝构造函数其实是一个构造函数的重载。
2.拷贝构造函数的参数必须使用引用传参,使用传值方式会引发无穷递归调用。
3.若未显示定义,系统会默认缺省的拷贝构造函数,缺省的拷贝构造函数会依次拷贝类成员进行初始化。
代码示例:
class Date
{
public:
Date()
{}
//拷贝构造函数
Date(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate1()
{
Date d1;
//下面两种用法都是调用拷贝构造函数,是等价的
Date d2(d1);
Date d3=d1;
}
三.析构函数
当一个对象生命周期结束时,C++编译系统会自动调用一个成员函数,这个特殊的成员函数即析构函数
析构函数是特殊的成员函数,其特征如下:
1.析构函数在类名前加上字符~
2.析构函数无参数无返回值。
3.一个类有且只有一个析构函数。若未显示定义,系统会自动生成缺省的析构函数。
4.对象生命周期结束时,C++编译系统自动调用析构函数。
5.注意析构函数体内并不是删除对象,而是做一些清理工作。
class Date
{
public:
//析构函数
~Date()
{}
private:
int _year;
int _month;
int _day;
};
//EXOP
class Array
{
public:
Array(int size)
{
_ptr=(int*)malloc(size*sizeof(int));
}
//这里的析构函数需要完成清理工作
~Array()
{
if(_ptr)
{
free(_ptr);
_ptr=0;
}
}
private:
int*_ptr;
};
运算符重载
为了增强程序的可读性,C++支持运算符重载。
运算符重载的特征:
1.operator+合法的运算符构成的函数名(重载<运算符的函数名:operator<)。
2.重载运算符以后,不能改变运算符的优先级/结合性/操作数个数
四.赋值运算符重载
拷贝构造函数是创建的对象,使用一个已有对象来初始化这个准备创建的对象。
赋值运算符重载是对一个已存在的对象进行拷贝赋值
class Date
{
public:
Date()
{}
//拷贝构造函数
Date(const Date& d)
:_year(d._year)
,_month(d._month)
,_day(d._day)
{}
Date& operator=(Date& d)
{
if(this!=&d)
{
this->_year=d._year;
this->_month=d._month;
this->_day=d._day;
}
return *this;
}
private:
int_year;
int_month;
int_day;
};
void Test()
{
Date d1;
Date d2=d1;//调用拷贝构造函数
Date d3;
d3=d1;//调用赋值运算符的重载
}