如果每个人都能理解你,那你得普通成什么样子。
前言
这是我自己学习C++的第二篇博客总结。后期我会继续把C++学习笔记开源至博客上。
上一期笔记是关于C++的入门基础知识,没看的同学可以过去看看:
关于C++入门基础知识的笔记
https://blog.csdn.net/hsy1603914691/article/details/142715432?spm=1001.2014.3001.5502
类的定义
类定义的格式
1. class为定义类的关键字。比如:定义一个栈结构,class为定义栈类的关键字,Stack为栈类的名字,{ }里面为栈类的主体,注意栈类定义结束时,后面的;不能省略。
2. 类的主体 中的内容称为 类的成员 :类中的 变量 称为 类的属性(成员变量) ;类中的 函数 称为 类的方法(成员函数) 。3. 为了区分成员变量,成员变量会加一个特殊标识,如 成员变量 前面加上 ' _ ' 下划线 。4. C++ 中 struct 也可以定义类, C++ 兼容 C 中 struct 的用法,同时 struct升级成了类 ,明显的变化是 struct中可以定义函数 ,一般情况下我们还是推荐用 class 定义类。5. 定义在类里面的 成员函数 默认为 存在inline内联修饰 ,运行效率更高。
class class_name
{
//类的主体
};
访问限定符
1. C++一种实现封装的方式,就是用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限,选择性的将其接口提供给外部的用户使用。
2. public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问。
3. 访问权限作用域从该访问限定符出现的位置开始,直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 ' } ',即类结束。
4. 一般来说,对象的属性是私有的,对象的方法是共有的。
5. class定义类的时候,没有被访问限定符修饰的部分默认为private,而struct定义类的时候,没有被访问限定符修饰的部分默认为public。
6. 一般来说,我们通过类的函数来改变类的变量,使得行为更加规范可靠。
#include <iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init()
{}
void Push()
{}
int Top()
{}
void Destroy()
{}
private:
// 成员变量
int* arr;
size_t capacity;
size_t top;
};
int main()
{
Stack st;//创建一个栈类型的变量,命名为st
st.Init();//初始化st
st.Push(1);//尾插st
st.Push(2);//尾插st
cout << st.Top() << endl;//打印st的首元素
st.Destroy();//销毁st
return 0;
}
类域
1. 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外使用成员时,需要使用 :: 指明成员属于哪个类域。
2. 当类域中的函数定义过长时,则在类域中存放函数声明,在类域外面定义函数。
3. 类域影响的是编译的查找规则,下面程序中Init()函数如果不指定类域Stack,那么编译器就把Init()函数当成全局函数,那么在编译时,找不到函数中成员变量和成员函数的声明、定义,就会报错;如果指定类域Stack,就是知道Init()函数是成员函数,当前域中找不到函数中那些成员变量和成员函数,就会到类域中去查找。
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init(int n = 4);
private:
// 成员变量
int* arr;
int capacity;
int top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)//指定类域Stack::Init,并且当存在缺省值时,只能在函数声明处加上形参的缺省值
{
//实现函数
}
int main()
{
Stack st;
st.Init();
return 0;
}
实例化
实例化概念
1. 类在物理内存中创建对象的过程,称为类实例化出对象。
2. 类是对象的一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量。这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
3. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
对象大小
1. 类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量。但是成员函数并没有保存在每个对象里面。
2. 所以计算类的大小时候,只计算成员变量占用的内存,并不计算成员函数占用的内存。
3. C++规定类实例化的对象也要符合内存对齐的规则。
4. 如果是没有成员变量的类对象,则占一个字节,表示占位,但不存储有效数据。
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
//...
}
private:
char _ch;
int _i;
};
class B
{
public:
void Print()
{
//...
}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;//8
cout << sizeof(b) << endl;//1
cout << sizeof(c) << endl;//1
return 0;
}
this指针
1. 编译器编译后,类的成员函数都会默认在形参第一个位置增加一个当前类类型的指针,叫做this指针。
2. 类的成员函数访问成员变量,本质上都是通过this指针访问的。
3. C++规定不能在形参的位置上显示的写this指针,但是可以在函数体内使用this指针,一般作为赋值运算符重载函数的返回值。
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year,int month,int day)//本质上是void Init(Date* this,int year,int month,int day)
void Print()//本质上是void Print(Date* this)
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 11, 3);//本质上是d1.Init(&d1, 2024, 11, 3)
d1.Print();//本质上是d1.Print(&d1)
Date d2;
d2.Init(2024, 11, 4);//本质上是d2.Init(&d1, 2024, 11, 4)
d2.Print();//本质上是d1.Print(&d2)
}
封装
1. C++把变量和函数都放到了类里面,通过访问限定符进行限制,用户不能再能随意通过对象直接修改数据,这是C++封装的主要体现。
2. C++封装中有一些相对方便的语法:调用成员函数时每次不需要传对象地址,因为this指针隐含的传递了;使用类类型不再需要typedef,直接使用类名即可。
类的默认成员函数
1. 默认成员函数,就是即使用户没有写代码去实现,编译器也会自动生成的成员函数。
2. 对于一个类,一般情况下,编译器会生成六个默认成员函数。我们主要学习前面四个默认成员函数,对于后面两个默认成员函数只需了解即可。
构造函数
构造函数的理解
1. 构造函数的主要任务并不是去创建对象,而是给对象进行初始化。
2. 构造函数是要替代以前写的Init()函数的功能,构造函数自动调用的特点就完美的替代了手动调用Init()函数。
构造函数的特性
1. 构造函数的函数名与类名相同。
2. 构造函数无返回值,也不需要在最前面写void。
3. 构造函数可以实现函数重载。(无参数,有参数,缺省参数)
4. 对象实例化时,系统会自动调用对应的构造函数。
5. 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,但编译器生成的默认构造函数一般都用不了!!!
6. 一旦用户显示定义构造函数,那么编译器将不再生成默认构造函数。
7. 对于内置类型成员变量,比如:int、char,需要我们显示的实现构造函数,这样才能在对象实例化中实现对象的初始化。
8. 对于自定义类型成员变量,比如:MyQueue,也需要我们显示的实现构造函数,这样才能在对象实例化中实现对象的初始化。但是对于自定义类型成员变量,我们更多的使用初始化列表来初始化它们。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//在实例化对象d1的时候,会自动调用构造函数。
d1.Print();//1 1 1
return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
Date()//无参数的构造函数
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 1, int month = 1, int day = 1)//全缺省的构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(int year, int month, int day)//有参数的构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1, 1, 1);//在实例化对象d1的时候,会自动调用构造函数。至于调用哪种构造函数,则遵循函数重载。
d1.Print();
return 0;
}
析构函数
析构函数的理解
1. 析构函数 与 构造函数 功能相反,构造函数 不是用来创建对象 ,析构函数 也不是用来完成对象本身的销毁 。2. C++ 规定 对象在销毁时 会 自动调用析构函数 ,完成对象中资源的清理释放工作。
析构函数的特点
1. 析构函数的函数名就是 '~类名'。
2. 析构函数无参数,无返回值,也不需要在最前面加void。
3. 一个类只能有一个析构函数。
4. 若用户没有显示实现,则系统会自动生成默认的析构函数。对象生命周期结束时,系统会自动调用析构函数。
5. 对于内置类型成员变量和自定义类型成员变量,析构函数可以不写,可以直接使用编译器生成的默认析构函数;但是当存在动态资源资源申请时,需要自己写析构函数,否则会造成资源泄漏。
8. 一个局部域的多个对象,C++规定后定义的对象先析构。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
~Date()
{
cout << '1' << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1, 1, 1);//在实例化对象d1的时候,自动调用无参的默认构造函数。
d1.Print();
return 0;//d1的生命周期结束时,会自动销毁,此时会自动调用析构函数,清理内存资源空间
}
拷贝构造函数
拷贝构造函数的理解
1. 如果一个构造函数的第一个参数是自身类类型的引用,且任何参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造函数的特性
1. 拷贝构造函数是构造函数的一个重载。
2. 拷贝构造函数的第一个参数必须是自身类类型对象的引用。如果使用传值传参方式,编译器直接报错,因为语法逻辑上会引发无穷递归调用。此时自身类类型对象的引用推荐使用const限制,既可以防止引用发生修改,又可以确保常量对象可以正常传入。
3. 拷贝构造函数可以有多个参数,但是第一个参数必须是自身类类型对象的引用,后面的参数必须有缺省值。
4. 若用户没有自己写代码定义拷贝构造函数,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量、自定义类型成员变量会完成值拷贝、浅拷贝;但是当存在动态资源资源申请时,需要我们自己去写拷贝构造函数。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//隐藏了this指针
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
~Date()
{
cout << '1' << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1, 1, 1);//在实例化对象d1的时候,直接调用有参数的默认构造函数。
d1.Print();
Date d2(d1);//自动调用拷贝构造函数。构造函数存在重载,由于参数不同,故系统通过参数来判需要自动调用的是哪一个构造函数。
Date d3 = d1;//拷贝构造函数的另一种调用形式。
d2.Print();
d3.Print();
return 0;//d1、d2、d3的生命周期结束时,会自动销毁,此时会自动调用析构函数,清理内存资源空间
}
赋值运算符重载函数
一般运算符重载
1. 当运算符被用于类类型对象时,C++允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须调用对应的运算符重载函数,若没有对应的运算符重载函数,则会编译报错。
2. 运算符重载函数的函数名是由operator+要重载的运算符共同构成。
3. 运算符重载函数的参数个数和该运算符作用的运算对象数量一样多。如果运算符重载函数是成员函数,则第一个运算对象默认传给隐式的this指针,因此运算符重载函数作为成员函数时,参数比运算对象数量少一个。
4. 运算符重载以后,其优先级和结合性应该与原本运算符保持一致。
5. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
6. ' .* ' , ' :: ' , ' sizeof ' , ' ? : ' , ' . ' 注意以上5个运算符不能重载。
7. 运算符重载函数不能通过运算符重载改变内置类型对象的含义,比如:不能出现 int operator+(int x, int y)。
8. 一个类需要重载哪些运算符,需要看哪些运算符重载后有意义,比如:Date类重载operator-有意义,但是重载operator*就没有意义。
9. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
10. 重载<<和>>时,需要重载为全局函数。因为如果重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 " 对象<<cout ",不符合使用习惯和可读性;如果重载为全局函数,把ostream、istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
11. 重载<<和>>时,需要重载为全局函数,但是又需要访问类内部成员变量,所以输入输出的重载函数需要成为类的友元。
bool operator<(const Date& d)// 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。
{
if (_year < d._year)
{
return true;
}
else if ((_year < d._year) && (_month < d._month))
{
return true;
}
else if ((_year < d._year) && (_month < d._month) && (_day < d._day))
{
return true;
}
else
{
return false;
}
}
赋值运算符重载
1. 赋值运算符重载函数是默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值。这里要注意跟拷贝构造区分,拷贝构造用于一个已经存在的对象拷贝给另一个刚刚创建的对象。
2. 赋值运算符重载,规定必须重载为成员函数。
3. 类类型作为形式参数时,一般传地址而非传值,而传地址时更推荐用引用而非指针(更加简洁,避免空指针),我们在形参前面加上const修饰(防止引用发生修改,确保常量对象可以正常传入)。
4. 赋值运算符重载函数有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值的场景,而且函数内需要写成return *this。
5. 若用户没有自己写代码定义赋值运算符重载函数,编译器会自动生成赋值运算符重载函数。自动生成的赋值运算符重载对内置类型成员变量、自定义类型成员变量会完成值拷贝、浅拷贝;但是当存在动态资源资源申请时,需要我们自己去写拷贝构造函数。
Date& operator=(const Date& d)//隐藏了this指针
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
1. 构造函数:需要自己写代码实现构造函数 。
2. 析构函数、拷贝构造函数、赋值运算符重载函数:当存在动态资源资源申请时,需要自己写代码实现这些函数。
3. 运算符重载函数:需要自己写代码实现运算符重载函数。
取地址运算符重载
const成员函数
1. 被const修饰的成员函数称为const成员函数,定义const成员函数时,需要把const放到成员函数参数列表的后面。
2. const实际上修饰的是该成员函数隐含的this指针,表明在该成员函数中不能修改对象的任何成员的内容。
3. const修饰指针本身的时候,不能改变指针指向的对象,但是可改变指针指向的内容;const修饰指针指向的内容的时候,可以改变指针指向的对象。其中,后者才会触发权限的放大和缩小。
4. 如果我们要求修改类的成员变量,就不能调用const成员函数;如果我们不要求修改类的成员变量,那么就建议都调用const成员函数。
5. const成员函数的返回类型,如果是非引用类型,则不能修改;如果是常量引用类型,则也不能修改;如果是非常量引用类型,则可以修改。
6. const对象只能调用const成员函数!!!
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
//void Print(Date* const this) //虽然有const修饰,但是修饰的是指针本身,即this指针只能指d2,不能改变指向的对象,但可以改变指针的内容
//{
// cout << _year << " " << _month << " " << _day << endl;
//}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(5, 5, 5);
const Date d2(4, 4, 4);//此时d2的指针是 const Date* d2; 即d2的数值不能进行修改
d1.Print();
d2.Print();
}
取地址运算符重载
1. 取地址运算符重载,分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
日期类的实现
<date.h>文件
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Date
{
public:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
Date(int year = 1, int month = 1, int day = 1)//内置类型必须自己实现构造函数
{
_year = year;
_month = month;
_day = day;
}
bool operator<(const Date& d);
bool operator>(const Date& d);
bool operator=(const Date& d);
bool operator<=(const Date& d);
bool operator>=(const Date & d);
bool operator!=(const Date& d);
int get_month_day();
Date& operator+=(int day);
Date& operator+(int day);
Date& operator-=(int day);
Date& operator-(int day);
Date& operator++();//前置++
Date& operator++(int i);//后置++
void Print();
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
<date.cpp>文件
#include "date.h"
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if ((_year == d._year) && (_month < d._month))
{
return true;
}
else if ((_year == d._year) && (_month == d._month) && (_day < d._day))
{
return true;
}
else
{
return false;
}
}
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
bool Date::operator=(const Date& d)
{
if ((_year == d._year) && (_month == d._month) && (_day == d._day))
{
return true;
}
else
{
return false;
}
}
bool Date::operator<=(const Date& d)
{
return (*this = d) && (*this < d);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
bool Date::operator!=(const Date& d)
{
return !(*this = d);
}
int Date::get_month_day()
{
static int month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (((_year % 4 == 0) && (_year % 100 != 0))|| (_year % 400 == 0))
{
if (_month == 2)
{
return 29;
}
else
{
return month[_month];
}
}
else
{
return month[_month];
}
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day = day + _day;
while (_day> (*this).get_month_day())
{
_day = _day - (*this).get_month_day();
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date& Date::operator+(int day)
{
Date newdate(*this);
if (day < 0)
{
return newdate -= -day;
}
newdate._day += day;
while (newdate._day > newdate.get_month_day())
{
newdate._day -= newdate.get_month_day();
newdate._month++;
if (newdate._month == 13)
{
newdate._year++;
newdate._month = 1;
}
}
return newdate;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year -= 1;
_month = 12;
}
_day += (*this).get_month_day();
}
return (*this);
}
Date& Date::operator-(int day)
{
Date newdate(*this);
if (day < 0)
{
return newdate += -day;
}
newdate._day -= day;
while (newdate._day <= 0)
{
newdate._month--;
if (newdate._month == 0)
{
newdate._year -= 1;
newdate._month = 12;
}
newdate._day += (*this).get_month_day();
}
return newdate;
}
Date& Date::operator++()//前置++
{
*this += 1;
return (*this);
}
Date& Date::operator++(int i)//后置++
{
Date newdate(*this);
*this += 1;
return (newdate);
}
void Date::Print()
{
cout << " " << _year << " " << _month << " " << _day << " " << endl;
}
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)
{
++min;
n++;
}
return n * flag;
}
ostream& operator<< (ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>> (istream& in, Date& d)
{
cout << "请输入年月日:";
in >> d._year >> d._month >> d._day;
return in;
}
<test.cpp>文件
#include "date.h"
int main()
{
//Date d1;
//d1.Print();//1 1 1
//d1 += 31;
//d1.Print();//1 2 1
//Date d2 = (d1 + 1);
//d1.Print();//1 2 1
//d2.Print();//1 2 2
//d2 -= 1;
//d2.Print();//1 2 1
//d2 -= 31;
//d2.Print();//1 1 1
//Date d3=d2++;
//d3.Print();//1 1 1
//d2.Print();//1 1 2
//Date d4=++d2;
//d4.Print();//1 1 3
//d2.Print();//1 1 3
Date d1(1, 1, 1);
Date d2(8, 5, 5);
cout << d1;
operator<<(cout, d1);
cin >> d1 >> d2;
cout << d1 << d2;
return 0;
}
致谢
感谢您花时间阅读这篇文章!如果您对本文有任何疑问、建议或是想要分享您的看法,请不要犹豫,在评论区留下您的宝贵意见。每一次互动都是我前进的动力,您的支持是我最大的鼓励。期待与您的交流,让我们共同成长,探索技术世界的无限可能!