目录
类的定义
类是从C语言结构体的基础上发展而来的。
class是定义类的关键字,后面接类的名字,然后在接一对 {} ,注意要在 } 的后面接一个分号(class可以被struct替换)。
class A
{
//...
};
在类体中可以存在变量和函数,类的内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法或成员函数(类里定义的函数默认是inline)。
在定义成员变量时建议加一个特殊标识,我一般在变量前加_。这么做是为了方便认出成员变量。具体如下:
//没加特殊标志
class A
{
void Init(int a1, int a2)//初始化
{
a1 = a1;
a2 = a2;
}
int a1;
int a2;
};
//加特殊标志
class A
{
void Init(int a1, int a2)
{
_a1 = a1;
_a2 = a2;
}
int _a1;
int _a2;
};
类定义了一个新的作用域一般称为类域,如果想在类外使用成员时就要用作用域操作符(::)来指明成员属于哪个类域。
访问限定符
访问限定符有3个:public、protected、private。
被public修饰的成员是公有即在类外是可以被直接访问,被protected,private修饰的成员是私有即在类外是不能被直接访问(它们之间的区别之后在说)。
下面是访问限定符的几个特征:
- 访问权限的作用域从该访问限定符出发到下一个访问限定符,如果后面没有访问限定符,作用域到 } 结束。
- class在没有访问限定符修饰时默认是私有,struct默认是公有。
- 一般来说成员变量都会被设置为私有,需要给别人使用的函数会被设置为公有。
所以上面的A可以改如下,事实上下面的写法更加合理:
class A
{
public:
void Init(int a1, int a2)
{
_a1 = a1;
_a2 = a2;
}
private:
int _a1;
int _a2;
};
封装
面向对象的三大特征:封装、继承、多态。
封装:将数据和操作数据的方法结合在一起,形成一个类。隐藏对象属性和实现细节,仅对外公开接口来和对象进行交换,也就是说对外提供接口,对内提供数据。
这样做有几个好处:
- 信息隐藏:保护类的内部数据不被外部直接访问和修改,增强了程序的安全性和稳定性。
- 模块化:将相关的数据和操作封装在一个单元中,提高了代码的可维护性和可重用性。
- 控制访问权限:可以精确地控制哪些数据和操作是对外部可见和可用的,哪些是内部私有的。
实例化
用类类型创建一个对象的过程,称为类的实例化(依然是以A举例)。
A aa;//实例化
类是对对象进行描述,它就像一个模型。类并没有分配实际的内存空间来存储它。实例化后它就有了实际的物理空间。
实例化后就可以使用类里面公开的内容了。
A aa;
aa.Init(1, 1);
类对象大小计算
在计算大小之前要先知道它是怎么存储的,由于类是由结构体发展而来的所以对象里一定会有成员变量,那是否包含成员函数?其实是不包含的。计算大小的方法就是套用了C语言里结构体里的内存对齐规则(详见结构体)。
就用上面的A计算它的大小,根据内存对齐规则得出大小为8个字节,那是不是呢?
可以看到是8个字节。那如果是嵌套一个4个字节的B类那么大小会是多少呢?
class A
{
public:
class B
{
private:
int _b;
};
private:
int _a1;
int _a2;
};
可以看到A的大小仍然是8个字节,这表明了嵌套的那个类(B类)会独自开辟一段空间。
注意:如果一个类里面如果没有成员变量,那它就是一个空类。编译器会给它一个字节的空间来唯一标识这个类的对象。
this
猜猜下面代码的运行结果。
class A
{
public:
void Init(int a1, int a2)
{
_a1 = a1;
_a2 = a2;
}
void Print()
{
cout << _a1 << ' ' << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A aa1;
A aa2;
aa1.Init(1, 1);
aa2.Init(2, 2);
aa1.Print();
aa2.Print();
return 0;
}
下面是代码得运行结果:
成员函数体内并没有对不同对象进行区分,但是输出结果却没有错误。这是为什么呢?事实上C++编译器会自动给每个“非静态的成员函数”加一个隐藏的指针参数(this)。具体如下:
上面画红线的地方都是编译器自己隐式完成的,我在这里写出来只是为了方便理解,我们在平时写的时候不需要写出来。
下面是this指针的几个特性:
- this被const修饰这就表明不能给this赋值。
- 只能在成员函数的内部使用this指针。
- this指针是成员函数的形参,它的位置在我们传入自己的第一个形参位置的前面。
- this指针一般存储在栈或者寄存器ecx上。
默认成员函数*
C++中的类通常有六个默认成员函数,即便没有显式地定义它们,编译器也会自动生成(如果我们显式地定义了编译器就不会自动生成了)。这六个默认成员函数是:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载、const 修饰的取地址操作符重载。那既然我们不写也会自动生成为什么还要介绍,其实在大多数的情况下编译器生成的不能满足我们的需求。
下面我会对前四个常见默认成员函数进行详细介绍:
构造函数
构造函数是一个特殊的成员函数,用于在创建类的对象时进行初始化操作(不是开空间创建对象)。它的名字与类名相同,没有返回值。当创建类的对象时,编译器会自动调用相应的构造函数并且在对象整个生命周期中只调用一次。
在了解构造函数后,它就可以替代我们之前写的初始化(Init)函数。
class A
{
public:
//构造函数
A(int a1, int a2)
{
_a1 = a1;
_a2 = a2;
}
private:
int _a1;
int _a2;
};
int main()
{
A aa(1, 1);
return 0;
}
这样既创建了对象又初始化了非常方便。 另外构造函数支持重载!这样就可以实现不同的初始化方式。
class A
{
public:
A(int a1, int a2)//有参构造函数
{
_a1 = a1;
_a2 = a2;
}
A()//无参构造函数
{
_a1 = 3;
_a2 = 3;
}
A(int a1 = 2, int a2 = 2)//全缺省构造函数
{
_a1 = a1;
_a2 = a2;
}
A(int a1, int a2 = 2)//半缺省构造函数
{
_a1 = a1;
_a2 = a2;
}
private:
int _a1;
int _a2;
};
注意:上面的第二个构造函数和第三个构造函数如果在没有传参的情况下一起出现就会出现对重载函数的调用不明确的错误。另外我们也可以在成员变量声明是给它默认值。
下面是构造函数的几个特征:
- 默认构造函数并不是完全指编译器自动生成的,它还包括全缺省构造函数和无参构造函数。并且默认构造函数只能有一个。
- 构造函数没有this指针,因为构造函数是创建对象的,在创建对象之前不存在对象的首地址。
虽然上面的构造函数调用之后,对象已经有了一个初始值,但构造函数体内并不能称之为初始化,准确的说应该是赋初值。因为初始化只能初始一次,而构造函数体内可以多次赋值。如果类里面有引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数)这三种中的任意一种,在构造函数体内不能对它们赋值,只能在它们初始化的时候给它们初始值。这时候就要用到初始化列表。
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个括号,括号中放初始值或表达式。
class A
{
public:
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
private:
int _a1;
int _a2;
};
下面是初始化列表的几个特征:
- 初始化列表中每个成员变量只能出现一次 。
- 上面提到的引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数)这三个必须放在初始化列表中。
- 尽量使用初始化列表初始化,因为每一个成员变量都会走初始化列表,就算初始化列表中没有写它。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
在上面我提到了可以在声明成员变量时给它默认值,那如果我们这样写输出结果会是什么?
class A
{
public:
A(int a1, int a2)
:_a1(a1)
{}
void Print()
{
cout << "_a1->" << _a1 << endl << "_a2->" << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
A aa(2,2);
aa.Print();
return 0;
}
结果是:-a1->2,_a2->1。怎么样写对了吗?那要怎么理解呢?
_a1虽然给了默认值1但是在初始化列表时_a1被a1初始化了。_a2虽然不在初始化列表但是根据上面的特性它也会走初始化列表,因为它没被初始化所以它是1。还有就是如果类里面有我提到的上面三个必须在初始化列表初始化时,一定要写它们的初始化(如果在声明的地方写了那就可以不在初始化列表写)。
析构函数
析构函数是一种特殊的成员函数。在对象生命周期结束时会自动调用析构函数,完成对象中资源的清理工作。
函数名是在类名前加上字符~,无参数无返回值,但有 this 指针,一个类有且只有一个析构函数,不能重载,若未显式定义,系统会自动生成默认的析构函数。具体如下:
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
//析构函数
~A()
{
_a1 = 0;
_a2 = 0;
}
private:
int _a1;
int _a2;
};
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
A类。有资源申请时,一定要写,否则会造成资源泄漏,比如定义了一个栈类。
拷贝构造函数
拷贝构造函数是构造函数的一个重载形式。通常只有一个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
//拷贝构造函数
A(const A& a)
:_a1(a._a1)
,_a2(a._a2)
{}
private:
int _a1;
int _a2;
};
int main()
{
A aa1(1, 1);
A aa2(aa1);//也可以换一种写法 A aa2 = aa1
return 0;
}
注意:这里只能是传引用不能传值,如果要是传值就会造成无限递归。因为C++规定传值调用会自动调用拷贝构造函数,但是它本身就是拷贝构造函数,所以会造成无限递归。
如果未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或者值拷贝。类中如果没有涉及资源申请时,拷贝构造函数是否写都可以。一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。大家可以看看如下代码运行结果:
class Stack
{
public:
Stack(int n = 4)
:_size(0)
, _capacity(n)
, _arr((int*)malloc(sizeof(int) * n))
{}
~Stack()
{
free(_arr);
}
void Print()
{
cout << _arr << endl;
}
private:
int* _arr;
int _size;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
st1.Print();
st2.Print();
return 0;
}
这是运行结果:
大家可以看到程序直接就崩了。为什么呢?在上面的代码中我并没有显示实现拷贝构造函数,编译器自己生成了一个默认的拷贝构造函数,它只会一个一个字节拷贝也就是浅拷贝,所以这两个对象里的_arr的地址一样。用free释放开辟的空间就只能释放一次,因为它们俩个都会走到析构函数且它们地址都一样同一片空间释放了两次所以就崩了。
综上所述只要涉及到资源申请时,拷贝构造函数是一定要写。下面是调用拷贝构造函数的典型的场景:
- 当用类的一个对象去初始化该类的另一个对象时。
- 当函数的形参是类的对象且进行形参和实参结合时。
- 当函数的返回值是对象且函数执行完返回调用者时。
赋值运算符重载函数
在介绍这个之前先说一下运算符重载。C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
运算符重载的名字是关键字operator后接要重载的运算符。有参数(参数里必须有类类型的参数)有返回值。
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
int operator+(const A& a)
{
return _a1 + _a2 + a._a1 + a._a2;
}
private:
int _a1;
int _a2;
};
int main()
{
A aa1(1, 1);
A aa2(aa1);
cout << aa1.operator+(aa2) << endl;//如果觉得这样写麻烦可以这样 aa1 + aa2
return 0;
}
下面是运算符重载的几个特征:
- 不能通过连接其他符号来创建新的操作符:比如operator@ 。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
赋值运算符重载是在两个已存在的对象之间进行赋值。与拷贝构造函数不同,拷贝构造函数是用已有对象给新生成的对象赋初值。另外C++规定赋值运算符只能重载成类的成员函数不能重载成全局函数。如果是编译器默认生成的,它就只会浅拷贝。只要不涉及资源申请就可以不写。这点和拷贝构造函数很像。
const成员函数
被const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
C++规定把const写在 ) 的后面,具体如下:
class A
{
public:
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print() const
{
cout << _a1 << ' ' << _a2 <<endl;
}
private:
int _a1;
int _a2;
};
只要成员函数不涉及对成员变量的改变,那就尽量把const加上。
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量。用static修饰的成员函数,称之为静态成员函数。静态成员变量规定要在类外进行初始化。
class A
{
public:
A(int a1)
:_a1(a1)
{}
private:
int _a1;
static int _a2;
};
int A::_a2 = 1;
下面是被static修饰成员和变量的几个特征:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
友元
在C++中,友元是一种特殊的机制。友元函数或友元类可以访问另一个类中的私有成员和保护成员。它定义在类外部,不属于任何类。虽然友元提供了遍历,但是会增加耦合度,会破坏了封装,所以不宜太多。
友元函数
友元函数是在某个类中声明的非成员函数,通过在类定义中使用关键字friend来声明。具体如下:
class A
{
friend void Print(const A& aa1);
public:
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
private:
int _a1;
int _a2;
};
void Print(const A& aa1)
{
cout << aa1._a1 << endl;
}
int main()
{
A aa1(1, 1);
return 0;
}
下面是友元函数的几个特征:
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用原理相同。
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class A
{
friend class B;
public:
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
void Print() const
{
cout << _a1 << endl;
}
private:
int _a1;
int _a2;
};
class B
{
public:
void P(const A& aa)
{
aa.Print();
}
private:
int _b;
};
注意:A类里的Print函数里的const并不是单单指上面提到的const成员函数,它的出现更重要是因为B类里的P成员函数里的参数被const修饰,所以aa里的内容是不能改变的。如果A类里的Print函数没用const修饰那就代表传入Print里的参数是可以修改的,这是不被允许的。
友元关系是单向的,不具有交换性。如果C是B的友元, B是A的友元,则不能说明C时A的友元。
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类是外部类的友元类。内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
class A
{
public:
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
class B
{
public:
void Print(const A& aa1) const
{
cout << aa1._a1 <<endl;
}
private:
int _b = 1;
};
private:
int _a1;
int _a2;
};
int main()
{
A aa1(1, 1);
A::B b;
b.Print(aa1);
return 0;
}
下面是内部类的几个特征:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
匿名对象
在C++中,匿名对象是指没有名字的对象。如果我们只想调用一下A类里的打印那我们可以这样写:
int main()
{
A aa1(1, 1);
aa1.Print();
return 0;
}
这样写还是有点麻烦,这时候就可以利用匿名对象。
int main()
{
A(1, 1).Print();
return 0;
}
注意:匿名对象的生命周期只在这一行。