c++相对于c来说,是面向对象的语言,相对于c更注重于解题的过程,c++更喜欢去将解题过程来做成一个模板,去探究不同题的解题方法,这样去研究。
所以c++提出了类和对象的概念,将所定义的数据,与数据的处理方式,放入一个类中,封装起来,那么这个类就是一个抽象的类别,如同人这个类,而每个人就是类的实例化,而人是由什么组成的呢,那就是成员变量,对这些组成部分该怎么操作,那就是对身体机能的操作,是成员方法,这便是封装的含义,不可能说是将各个组织器官暴露出来,这样每个人都能去操作别人的组织器官,那不乱套了吗?
那么类到底是什么呢,其实和结构体是差不多的,相对于定义方面来说,c++中也有struct定义类和结构体,但是多了一个访问权限,这样的实现使得封装可以出现,类中有public private protected三种访问权限,只有public允许在外部被访问。
而struct和class的区别是什么呢,两者都可以去定义类,但是class的默认权限是private,而struct是public。类创造了一个域,所以如果要访问,需要加上域作用符 ::
类的实例化,就是创建一个对象,类先声明,然后定义出对象。而成员函数的定义是可以在类外面去定义的,一般我们也是这么做的,类里声明,类外实现。
但是对于对象的大小计算来说,计算的只是成员变量的大小,函数是不去计算的,因为多个对象是可以调用一个成员函数的,计算方式和结构体一样,存在内存对齐的规则,但是无成员变量的类大小不是0是1,因为开一个字节占位,表示对象存在。
this指针,成员变量的访问是由隐含的this指针访问的。
void print()
{
cout << _year << _month << _day <<endl;
}
实质是
void print(Date* this)
{
cout << this->_year << this->_month << this->_day <<endl;
}
this指针是一个形参变量,一般存放在栈帧中,但是有些编译器会将它优化到寄存器上。
如果存在空指针访问,比如
class Date{
public:
void print()
{
cout << _year << _month << _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* p = nullptr;
p->print();
return 0;
}
这里的意思并非是解引用了空指针,而是print§,这种意思,就如同
Date d1;
d1.print();
这里也是 print(&d1);的意思。
默认成员函数:
构造函数 在对象构造时候调用用来初始化的函数,它函数名与类名相同,无返回值,对象实例化时候编译器自动调用,可重载。
编译器在没有实现这个函数的,会自动编写一个构造函数,但是这个构造函数有“双标”
它只会初始化自定义数据类型,而不会初始化内置类型,所以一般我们都会写构造函数,构造函数的自动调用特点是无参数,那么就用全缺省的参数,这样既可以自动初始化,也可以初始化成为我们想要的值。
Date (int year = 0,int month =1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
析构函数,是~类名的形式,无参数无返回值,只存在一个,在对象生命周期结束时自动调用,完成对象的资源清理,这里只是的是动态开辟的空间。因为建立在栈帧中所以,满足前进后出的调用。且,析构函数和构造函数一样,编译器去写会存在“双标”
拷贝构造,这个函数形式和构造函数完全一样,是构造函数的重载,参数是另一个对象,将这个对象的值初始化给新的对象。
Date(const Date& d1)
{
_year =d1._year;
_month = d1._month;
_day = d1._day;
}
这里需要注意的是,必须要用引用来传参,不然会造成死循环,如何理解呢,我们知道,形参传参的方式,是创建了一块空间,来将参数复制到这块空间中形成了形参,那么这中传递方式,不就是拷贝构造吗,那么这样就会陷入死循环。
如果我们不去写拷贝构造函数,那么编译器自动生成一个拷贝构造函数,那我们说它是值拷贝,或者浅拷贝,只是按字节序将值拷贝到新对象里,那么这里会出现一个浅拷贝问题。
例如是一个指针变量,那么只是将指针复制过去,那么会造成两个指针访问一个空间,且在析构的时候,一个空间不能够被析构两次,所以会发生这样的情况。
运算符的重载
运算符重载是为了让自定义类型也可以去使用运算符。一般形式为 返回值类型 operator操作符(参数列表),比如
bool operator==(const Date& d1)
{
return _year ==d1._year
&&_month = d1._month
&&_day = d1._day;
}
因为一般是在类里定义的,所以只传一个参数,另一个有this指针。而赋值运算符的重载,则系统会默认重载,但是也会有浅拷贝的问题,赋值运算符的重载要完成连续赋值,返回值也要是类的返回值
Date& operator=(const Date& d1)
{
_year =d1._year;
_month = d1._month;
_day = d1._day;
return *this;
}
这里还需要注意++ --运算符的重载,后置++加了个int形参,为了区别两者,并且,前置后置一个是返回加后值,一个是返回加前值。
Date& operator++()
{
;
return *this;
}
Date operator++(int)
{
Date ret = *this;
return ret;
}
前者是前置加加,后的是后置加加,这里没有实现。
如果要对this指针进行const修饰的话,就在函数后面加const
void print() const
{
cout << _year << _month << _day <<endl;
}
这样就是对this指针进行const修饰。因为const修饰的指针不能传给不被const修饰的参数,而反之可以,因为,权限可以被缩小但不能被放大。因此我们一般如果说实现函数的时候,如果不是要改变其中内容,一般加上const的修饰,这样为了以后函数调用函数的时候,能够更加方便。
const取地址以及取地址操作符的重载
这两种是默认成员函数,是编译器自动实现的,一般也不会自己去写。
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
初始化列表,构造函数中,我们上面的写法不能完全意义上叫做初始化,或者说只能说是赋初值,一般类中的成员变量叫做成员变量的声明,而对对象的定义时候,即对类的实例化时,我们要对成员变量进行初始化,初始化是在初始化列表上完成的:
Date (int year = 0,int month =1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
_year = year;
_month = month;
_day = day;
}
在上面中,冒号开头的是初始化列表,无论我们写不写它,它都会作用的,而体内的是赋值初始化,一般情况下有三种变量,只能用初始化列表初始化
引用变量,const变量,无默认构造函数的自定义类型变量。
因为这三种变量,必须初始化,不然无法构造,这样也可以理解,为什么说构造函数体内是赋值。一般我们构造函数是默认的,但是如果说构造函数被实现成不默认的,那么无法初始化了~
还有一种特殊情况
Date d1 = 2;
这种定义的方法是隐式类型转换,构造出一个Date tmp(2),然后让这个tmp拷贝构造出 d1 ,但有时候会直接优化成构造。但这种情况只能赋一个,那么c++11中实现了一个新玩法
Date d1 = {1,2,3};
使用花括号进行多个参数初始化,不过如果想防止这种初始化方式,可以使用explicit 修饰构造函数,那么这种方式就不顶用啦~
所以,我们在写构造函数的时候,一般尽量写初始化列表,因为无论写不写,初始化列表都会作用。并且,初始化顺序与声明顺序一样,和初始化列表写的顺序无关~
静态static 成员
我们可以使用static来修饰,成为静态成员变量,静态成员变量一定要在类外定义,静态成员变量属于类公有的变量,其定义在静态区,所以任何对象都可以访问它也可以用类名来进行域访问,而这里也可以看出来它是属于类的,不属于单个对象。
静态成员函数,没有this指针,所以不能访问非静态成员变量,因此:
静态成员函数不能调用非静态成员函数,因为没有this指针。
而反过来,非静态成员函数可以调用静态成员函数,但是需要突破类域和访问限定符。
c++11在定义成员变量的时候允许缺省
private:
int _year = 0;
int _month = 1;
int _day = 1;
友元
使用friend链接函数,这样函数便可以直接访问类中的私有成员变量,为什么不将友元函数直接定义到类里面呢,因为有时候我们需要将变量的一个位置留给别的参数,但是在类中,this指针是一定的第一个变量,比如要重载<<输出操作符,那么就需要第一个参数是osteram类型的变量,那么便需要友元函数的使用。
因此我们也清楚了,cout和cin是函数重载的结果,是iostream类中的成员函数。
一个类中要访问其他类的私有成员的话也需要这样的友元定义,如果一个类的成员变量有另一个类,那么就可以这么使用。
友元关系是不具有交换性的,是单向的,不能传递,友元会增强耦合度所以不宜多用。
内部类
在一个类中定义一个类,那么这个类就叫做这个外部类的内部类,外部类因为没有进入到内部类的域中,所以外部类对内部类没有任何优越的访问权限,而内部类进入到了外部类的域中,实质上,内部类就是外部类的友元类,因此内部类中可以访问任何的外部类的权限。
注意,内部类可以直接访问外部类的static和枚举成员,不需要任何别的标识,sizeof(外部类) = 外部类
匿名对象
Date();
这种直接用类名进行创建对象也是可以的,叫做匿名对象,只能在定义时候使用,所以当只需要在这一行去使用这个对象的时候,可以创建匿名对象,即它的生命周期只有这一行。
一般来说静态对象的析构和全局对象的析构,都是在栈中的对象之后的,这个很容易理解,因为生命周期不同,栈中是后进先出的原理,这里也不多讲了。
那么一般来说在main函数中的静态区变量是先析构的,在全局的静态区变量是后析构的。在构造时候,则是按照程序执行的顺序来构造的。
但是如果拷贝构造的话,会存在编译器的优化,在一个连续的动作时候,多次的拷贝构造会被优化成一个拷贝构造,一般是在传参和返回值的时候优化。