类和对象上
1.类的定义:
c语言中用struct关键字定义结构体,c++中同样可以用struct定义类,同样的更常使用class来定义类。c语言结构体中只能够定义成员变量,但是c++类中既可以定义成员函数,又可以定义成员变量。struct定义的类默认为public,而class定义的类默认为private。类域中成员变量的私有化体现了c++三大特性封装,继承,多态中的封装特性。将成员变量私有化,类里面可以访问,类外面不能访问,增加了访问安全性。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
类的两种定义方式:
1.声明和定义分离。类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
2.声明和定义全放子类中。需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处
理。
2.访问限定符
1.public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到} 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
3.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
4.类的实例化(创建对象)
1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。类域中的成员变量是声明而不是定义。
2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
5.类大小计算
成员函数不占内存,成员变量遵循内存对齐的原则。(成员函数存放在公共代码区)。
空类的大小为1,编译器给1个字节来唯一标识这个类的对象。
6.this指针
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针特性:
1. this指针的类型:const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
类与对象下
1.默认成员函数
默认成员函数有6个。
2. 构造函数(成员变量的初始化)
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
特性:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
默认构造函数:
可以分为3类,无参的构造函数和全缺省的构造函数以及我们没写系统自动生成的构造函数。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:
int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数而内置类型不处理。因此c++11打了补丁。内置类型成员变量在类中声明时可以给默认值。
任何类型的指针都是内置类型。
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
3.析构函数
概念:析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作.
特性:
1.函数名与类名相同前面加上~
2.无参数,无返回类型
3.不显示写系统会生成默认的析构函数,默认析构函数对内置类型不处理,对自定义类型调用他的析构函数。一个类只能有一个析构函数,析构函数不能重载。
4.对象生命周期结束会自动调用析构函数。内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。
5.大多数情况系统自动生成的析构函数就够用,但一些特殊场景,如动态开辟内存,需要我们自己释放,就需要显示的写析构函数。
4.拷贝构造
概念:拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特性:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数参数只有一个,且必须是类类型对象的引用,使用传值方式编译直接报错,会引发无穷递归。因为传值的过程形参是实参的拷贝 所以会触发无限递归。
3.如果不显示写编译器会生成默认的拷贝构造函数。对内置类型按照存储字节序依次拷贝,对自定义类型调用他的拷贝构造。 称为浅拷贝或值拷贝。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
经典使用场景
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
5.运算符重载(operator+运算符+())
c++中有时需要对自定义类进行运算,这时候内置类型的+-*\等通常不能用于对自定义类,因此需要对此运算符进行重载使其可以用运于自定义类型。
函数名字为:关键字operator后面接需要重载的运算符符号。
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符的重载:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数.
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
前置++和后置++重载
前置++
Date& operator++()
{
_day += 1;
return *this;
}
后置++
date operator++(int)
{
date ret(*this);
_day+=1;
return ret;
}
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用。
6.const
const修饰的"成员函数"成为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
一般在成员函数最后加const表示对this指针的修饰:::
void Date()
{}
//修饰前应为Date* const this
//修饰后为 const Date* const this
一般传参时使用const修饰,或者函数使用const修饰可以避免权限放大。
1. const对象可以调用非const成员函数吗? 不可以
2. 非const对象可以调用const成员函数吗? 可以
3. const成员函数内可以调用其它的非const成员函数吗? 不可以
4. 非const成员函数内可以调用其它的const成员函数吗? 可以
流插入和流提取重载!!!!!
流插入喝流提取的重载需要注意,因为类成员函数参数中有this指针的存在而operator<<是对两个操作符进行操作,this指针会占用第一个操作符。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
7.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。如可以将成员函数定义成私有,阻止访问。
8.初始化列表
对成员变量进行定义:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。类似于拷贝构造!!1
class Date
{
public:
int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注意:每个成员变量都只能在初始化列表初始一次。
类中包含以下变量只能在初始化列表进行初始化:
1.const 成员变量(const成员变量只能初始化一次)
2.引用成员变量(因为引用必须在创建时初始化)
3.自定义类型成员(且该类没有默认构造函数)(自定义类型在调用构造函数的时候首先会调用该自定义类型的默认构造函数,而该自定义类没有默认构造函数就需要初始化列表进行定义)。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
使用常量初始化时可以不用传参数。
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关 先声明的就先初始化。
9.explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
用explicit修饰构造函数,将会禁止构造函数的隐式转换。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)
:_year(year)
{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转
换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}
10.static成员
static 可以定义静态成员变量和静态成员函数。static定义的静态成员变量初始化时只能在全局初始化。
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
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;
}
特性:
1.静态成员变量为所有类对象共享,不属于具体某个对象,存放在静态区,生命周期在程序运行期间。
2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明.
3.类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5.静态成员也是类的成员,受public、protected、private 访问限定符的限制。
应用场景 :
1.定义静态成员变量可以避免定义全局变量大家都可以进行修改的尴尬!
2。限制对象的定义
问题:
1. 静态成员函数可以调用非静态成员函数吗? 不可以(静态成员函数没有this指针)
2. 非静态成员函数可以调用类的静态成员函数吗? 可以
11.友元函数(friend)
如果一个类想调用另外一个类的成员变量,则可以通过友元的方式来访问该类的成员变量。比如A类想访问B类的成员变量,那么A类要定义成B类的友元函数。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
结合流插入和流提取运算符重载理解:
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰(因为友元函数没有this指针)
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同,
友元类:
friend class Date
定义为友元类则该类可以直接访问包含友元类的私有成员变量。
友元关系不能继承 ,不能传递。
内部类:
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
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;
int main()
{
A::B b;
b.foo(A());
return 0;
}