目录
1.类和对象的理解
1.1 概念
C++是一个面向对象的语言,要了解学透C++,我们要先理解类和对象。
C++兼容C语言,结构体可以继续使用,类可以看做是结构体的升级版。
结构体:默认访问是公有的,结构体外部可以访问其内部。
类:默认访问是私有的,类的外部无法访问其内部,但可以自己设定为公有,让其访问。
以下是关于class(类)的说明
1.类的定义尾部有一个分号,代表定义结束,不能删掉!
2.一个类可以创建多个对象,每个对象都是变量。
3.类里面还可以定义一个新的类,称作为子类。
4.计算类的大小和结构体一样,也是需要字节对齐。
5.访问类可以通过.或者->进行访问。
struct student
{
int age;
char* name;
};
class student1
{
public:
class student2 //类中定义另一个类
{
public:
void Print()
{
cout << sex << endl;
}
public:
const char* sex;
};
void Print()
{
cout << "年龄是:" << age <<" " << "姓名是:" << name << endl;
}
public:
int age;
const char *name;
};
int main()
{
student s1;
s1.age = 10;
student1 s2;
s2.name = "小明";
s2.age = 13;
s2.Print();
student1::student2 s3;
s3.sex = "男";
s3.Print();
cout << sizeof(student) << endl;
cout << sizeof(student1) << endl;
return 0;
}
1.2 类的访问限定符
public:公有属性,在它的限定下的变量或者函数,类的内部和外部都能访问。
private:私有属性,在它的限定下的变量或者函数,只有类的内部能访问,类的外部不能访问。
protect:保护属性,在它的限定下的变量或者函数,只有类的内部或者子类才能访问。
1.3 类的成员
类有成员变量和成员函数。
并且对象是有类创建的。
class student
{
public:
void Print()
{}//成员函数
private:
int age;
const char* name; //成员变量
};
int main()
{
student s1; //s1即为student类创建的对象
return 0;
}
1.4 类的使用实例
创建一个Date类
Date.h
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year=2024,int month=7,int day=13);
void Print();
private:
int _year;
int _month;
int _day;
};
Date.cpp
#include "Date.h"
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
test.cpp
int main()
{
Date d1;
d1.Init();
d1.Print();
Date d2;
d2.Init(2023, 7, 13);
d2.Print();
}
2.类的对象细致讲解
什么是默认成员函数,是不用写。编译器能自动生成的函数
2.1默认成员函数介绍
构造函数
- 构造函数的函数名和类名是相同的 (比如类名是 Date,构造函数名就是 Date)。
- 构造函数无返回值 (它不具有返回类型,因此不能直接返回值)。
- 构造函数支持重载(下面有例子)。
Date() { //无参构造函数
_year = 0;
_month = 1;
_day = 1;
}
Date(int year=,int month=,int day=)//有参构造函数
{
cout << "Date()" << endl;
_year = year;
_month = month;
_day = day;
}
创建一个对象的d1,,当我们调用构造函数的时候,我们不能写d1.Date(),构造函数不是常规的成员函数,这样会显示报错。
如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。
#include <iostream>
class Date {
public:
void Print() {
cout<<_year<<_month<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1; // 这里调用的是默认生成的无参的构造函数
d1.Print();
return 0;
}
运行结果如下:
没有定义构造函数,对象也可以创建成功,因此此处调用的是 编译器默认生成的构造函数。
析构函数
析构函数也是特殊的成员函数
- 析构函数名是在类名前面加上字符
- 析构函数通俗的将就是帮我们“清理垃圾用的”。
- 析构函数既没有参数也没有返回值(因为没有参数,所以也不会构成重载问题)
- 一个类的析构函数有且仅有一个(如果不写系统会默认生成一个析构函数)
- 析构函数在对象生命周期结束后,会自动调用。
- 有一个特点,后定义的先析构
演示自动调用,在调用析构函数的时候"吱”一声
using namespace std;
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
~Date() {
cout << "~Date() 吱~ " << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
Date d2(2022, 3, 9);
return 0;
}
运行结果如下:
- 对于 "内置类型" 的成员变量:不会做初始化处理。
- 对于 "自定义类型" 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!
拷贝构造函数
- 拷贝构造函数是构造函数的一个重载。
- 参数只有一个,必须是同类型的对象。
- 必须要引用传参。
我们在创建对象的时候,能不能创建一个与某一个对象一模一样的新对象呢?
Date d1(2022, 3, 9);
d1.Print();
Date d2(d1); // 照着d1的模子做一个d2
d2.Print();
当然可以,这里我们就要用到拷贝构造。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
只有单个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
代码演示如下:
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date() {
cout << "~Date() 吱~ " << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
Date d2(2022, 3, 9);
Date d3(d2);
d3.Print();
return 0;
}
运行结果:
是不是完美复刻了d2的值,并且三个对象调用了三次析构函数
当然有一个重点呢!
拷贝构造为什么要用到引用传参呢?
调用拷贝构造,需要先穿参数,传值传参又是一个拷贝构造。
调用拷贝构造,需要先穿参数,传值传参又是一个拷贝构造。
调用拷贝构造,需要先穿参数,传值传参又是一个拷贝构造。
……
一直在传参这里出不去了,所以这个递归是一个无穷无尽的。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
~Date() {
cout << "~Date() 吱~ " << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
Date d2(2022, 3, 9);
Date d3(d2);
d3.Print();
return 0;
}
当我们省略了拷贝构造函数的时候,它还是拷贝出来了。那是不是代表我们不再需要些拷贝构造,编译器会自动帮我们写好。
当然是不行的,因为有些情况是不可避免的。
比如实现栈的时候,栈的结构问题,导致这里如果用默认的 拷贝构造,会翻车。
按字节把所有东西都拷过来会产生问题,如果 Stack st1 拷贝出另一个 Stack st2(st1)
会导致他们都指向那块开辟的内存空间,导致他们指向的空间被析构两次,导致程序崩溃
当遇到这种情况的时候,我们要给栈一个深拷贝
防止这块空间被析构两次
class stack
{
public:
stack(size_t n = 4)
{
cout << "stack()" << endl;
if (n == 0)
{
a = nullptr;
top = capacity = 0;
}
else
{
a = (int*)malloc(sizeof(int) * n);
if (a == nullptr)
{
perror("malloc fail");
exit(-1);
}
top = 0;
capacity = n;
}
}
stack(stack& s)
{
a = (int*)malloc(sizeof(int) * s.capacity);
if (a == NULL)
{
perror("malloc申请空间失败");
return;
}
memcpy(a, s.a, sizeof(int) * s.top);
top = s.top;
capacity = s.capacity;
}
void Init()
{
a = nullptr;
top = capacity = 0;
}
void push(int x)
{
if (top == capacity)
{
int newcapacity = capacity == 0 ? 4 : capacity * 2;
int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
if (tmp == nullptr)
{
perror("realloc fail");
exit(-1);
}
if (tmp == a)
{
cout << "原地扩容" << endl;
}
else
{
cout << "异地扩容" << endl;
}
a = tmp;
capacity = newcapacity;
}
a[top++] = x;
}
void pop()
{
assert(top > 0);
top--;
}
int Top()
{
return a[top - 1];
}
bool Empty()
{
return top == 0;
}
void Destroy()
{
free(a);
a = nullptr;
top = capacity = 0;
}
private:
int* a;
int top;
int capacity;
};
int main()
{
stack s1;
s1.push(1);
s1.push(2);
s1.push(3);
stack s2(s1);
return 0;
}
赋值运算符重载
运算符重载包含opreator< operator> operator== operator+= 等等
这些运算符能够帮助我们进行日期类的对比
bool Date::operator<(const Date& d)
{
if (this->_year < d._year)
{
return true;
}
else if (this->_year == d._year && this->_month < d._month)
{
return true;
}
else if (this->_year == d._year && this->_month == d._month && this->_day < d._day)
{
return true;
}
return false;
}
bool Date:: operator==(const Date& d)
{
return this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day;
}
bool Date:: operator<=(const Date& d)
{
return *this < d || *this == d;
}
int Date::Getmonthday(int year, int month)
{
int MonthArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
{
return 29;
}
return MonthArr[month];
}
Date& Date::operator+=(int day)
{
_day += day;
while (_day > Getmonthday(_year, _month))
{
_day -= Getmonthday(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
Date& Date:: operator=(const Date& d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_month--;
if (_month ==0)
{
_year--;
_month = 12;
}
_day += Getmonthday(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
Date& Date::operator++()
{
return *this += 1;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator--()
{
return *this -= 1;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min<max)
{
n++;
min++;
}
return n * flag;
}
2.2匿名对象
类名()//匿名对象,代码执行完毕,立即释放(不用到return)
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
cout<<"Test的无参构造函数"<<endl;
}
~Test()
{
cout<<"Test的有参构造函数"<<endl;
}
};
int main(int argc, char const *argv[])
{
Test(); //匿名对象,本行代码执行完,立即被释放
cout<<"*******************"<<endl;
return 0;
}
2.3 this指针
this是一个形参,一般存在栈桢里。
在同一个类中,this指向的同一个地址。
this的底层是指针。
在运算符重载里面,作为成员函数重载时,形参看起来比操作数数目少1,因为有隐藏的参数this
bool Date::operator<(const Date& d)
{
if (this->_year < d._year)
{
return true;
}
else if (this->_year == d._year && this->_month < d._month)
{
return true;
}
else if (this->_year == d._year && this->_month == d._month && this->_day < d._day)
{
return true;
}
return false;
}
3.静态成员变量和静态成员函数
3.1静态成员变量
(1)普通函数可以访问静态成员变量
(2)静态成员变量一定要在类的外部初始化
(3)静态成员变量可以直接通过类名来访问
在c++中,静态成员变量属于某个类,而不属于某个对象,我们可以使用静态成员变量来实现多个对象共享数据的目标
class Student
{
static int m_num;
};
3.2静态成员函数
(1)静态成员函数中只能访问静态成员变量,不能访问普通变量
(2)静态成员函数中的静态成员变量可以通过类名访问
(3)普通成员函数不能通过类名访问
#include <iostream>
using namespace std;
class Student
{
public:
static int count;
private:
int id;
public:
Student()
{
count++;
id = count;
}
int GetCount() //普通函数可以访问静态成员变量
{
return count;
}
static int GetCount1() //静态成员函数
{
return count; //静态成员函数中只能访问静态成员变量,不能访问普通变量
}
};
int Student::count = 0; //静态成员变量一定要在类的外部初始化
int main(int argc, char const *argv[])
{
Student s1;
Student s2;
cout<<s1.count<<endl;
cout<<s2.count<<endl;
cout<<Student::count<<endl; //静态成员变量可以直接通过类名来访问
cout<<Student::GetCount1()<<endl;
cout<<s1.GetCount()<<endl;
return 0;
}
4.友元函数与友元类
4.1友元函数
在当前类的外部定义、不属于当前类的函数也可以在类中声明,但是要在前面加关键字friend, 这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
友元函数可以访问当前类中的所有成员,包括public、protected、private等属性的成员。
#include <iostream>
using namespace std;
class Test
{
friend void show(Test &t); //将show函数声明为Test的友元,show可以访问Test的所有成员变量
private:
int m_a;
public:
void set(int a)
{
m_a = a;
}
};
void show(Test &t)
{
cout<<t.m_a<<endl;
}
int main(int argc, char const *argv[])
{
Test t1;
t1.set(2);
show(t1); //通过友元函数可以访问,Test类中的所有成员。
return 0;
}
4.2友元类
如果将类B声明为类A的友元类,那么类B中的所有成员函数都是类A的友元函数,类B就可以访问类A的所有成员,包括public、protected、private属性成员。
#include <iostream>
using namespace std;
class A
{
friend class B; //声明B为A的友元,友谊具有单向性,不代表A是B的友元,破坏了他的封装性
private:
int m_a;
public:
void set(int a)
{
m_a = a;
}
};
class B
{
private:
int m_b;
public:
void print(A &a)
{
cout<<"m_a = "<<a.m_a<<endl;
}
};
int main(int argc, char const *argv[])
{
A a1;
a1.set(2);
B b1;
b1.print(a1); //当B成为A的友元类后,类B的所有成员函数都是A的友元函数,所以通过B的函数
return 0; //可以访问A中所有成员。
}