C/C++总述:Study C/C++-CSDN博客
目录
C++类定义
C语言典型的面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是典型的基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类的结构:
class ClassName {
public:
// 公有成员函数和变量
// 可以被类外部访问
private:
// 私有成员函数和变量
// 只能被类内部成员函数访问
protected:
// 保护成员函数和变量
// 类的继承者可以访问
};
class为定义类的关键字,ClassName为类的名字,
{ }
中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员;类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
类的定义:
声明定义分离(推荐):
类声明放在.h文件中,成员函数定义放在.cpp文件中
注意:成员函数名前需要加类名 : :
成员变量命名规则(推荐):
在成员变量前加_
类的访问限定符和封装
面向对象的三大性质:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
封装防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。
注意:
一个类可以有多个 public、protected 或 private 标记区域。
每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。
成员和类的默认访问修饰符是 private。
访问限定符说明:
1.public修饰的成员在类外可以直接被访问。
2.protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。
4.class的默认访问权限为private,struct为public(因为struct要兼容C,C在struct外都能访问)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
类的实例化
类的实例化就是用类创建对象的过程
类是对象描述的一个东西一个模板。这里面只限定了类有哪些成员,定义出了一个类,但是并没有分配实际的空间来储存它。一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
比如 房屋建造的图纸就是类,根据图纸建造的房屋就是对象。
eg.
int main()
{
Date now; // 创建一个对象
now._year = 2024; // 调用该对象的成员并赋值
}
类对象模型
计算类的大小(遵循内存对齐原则)
//既有 成员函数 又有 成员变量
class A1
{
public:
void f1(){}
private:
int _a;
};
//只有 成员函数
class A2
{
public:
void f2(){}
};
//空类
class A3
{
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
return 0;
}
运行结果为:
4
1
1
C++规定:没有成员变量的类对象,它的大小为1字节,占位标识类存在过
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
注意:空类的大小,空类比较特殊,编译器给了空类1个字节来唯一标识这个类的对象。
规则:
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值,VS中默认的对齐数为8
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
类的对象储存方式
C++采用类对象中只存储成员变量,成员函数存放在公共代码段中的方式来存储类的对象。
this指针
在 C++ 中,this 指针是一个特殊的指针,它指向当前对象的实例。
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。
this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。
友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。
示例:
#include <iostream>
class MyClass {
private:
int _value;
public:
void setValue(int value) {
this->_value = value;
}
void printValue() {
std::cout << "Value: " << this->_value << std::endl;
}
};
int main() {
MyClass obj;
obj.setValue(42); //实际为obj.setValue(&obj,42);
obj.printValue();
return 0;
}
运行结果为:
Value: 42
因为编译器做了处理,setValue函数的入参不是1个参数,实际上是2个参数,编译器会增加一个隐含的参数:this指针,编译器会把对象的地址传给this。
编译器增加的隐含的this指针参数,即void setValue(Date* this, int value),作为形参,this指针存在于栈中。
经典问题
下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
这里变量p是一个类的指针,把它置为nullptr,然后把它当做指针,赋给this指针,在语法上并没有错误所以不会编译出错,然后访问函数Print,但是访问的时候,他并没有对this指针所指向的内容进行解引用(访问)
所以:选C
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
这里调用了函数,打印a,就着nullptr作为形参传递给this指针,进行解引用,就会运行崩溃!
类的6个默认成员函数
假如一个类中既没有成员变量也没有成员函数,那么这个类就是空类,空类并不是什么都没有,因为所有类都会生成如下6个默认成员函数:
构造函数
1.构造函数的定义及其特性
对于日期类对象,我们可能会忘记调用Init函数进行初始化,C++为了解决这个问题,引入构造函数进行初始化。
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int main()
{
Date d1;
d1.Init(2024, 2, 11);
d1.Print();
return 0;
}
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
特性:
1.构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
2.其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了编译器将不再生成!
3.默认构造函数是我们不传参就可以调用的函数
- 我们什么都没写,编译器自动生成的
- 我们自己写的:无参的构造函数
- 我们自己写的:全缺省构造函数
这三类只能存在一个,注意后两个不能同时存在的原因:当定义一个不带参数的类对象时,编译器不能确定到底要调用我们写的无参默认构造函数还是要调用我们写的带参全缺省默认构造函数,会报“对重载函数的调用不明确错误”。
(1) 我们什么都没写,编译器自动生成的
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用编译器自动生成的默认构造函数
return 0;
}
(2)我们自己写的:无参的构造函数
#include<iostream>
using namespace std;
class Date
{
public:
//1.无参默认构造函数:初始化对象
Date()
{
_year = 2024;
_month = 2;
_day = 12;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
(3)我们自己写的:全缺省构造函数
#include<iostream>
using namespace std;
class Date
{
public:
//2.带参全缺省默认构造函数:初始化对象
Date(int year = 2024, int month = 2, int day= 12)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用带参默认构造函数
return 0;
}
2、编译器自动生成的默认构造函数
C++把类型分成内置类型(基本类型)和自定义类型。
编译器生成默认的构造函数对内置类型不做处理
而对自定义类型成员会调用的它的默认成员函数
析构函数
1、析构函数的定义及其特性
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
特性:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。(析构函数不能重载,一个类有且仅有一个析构函数)
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时(出作用域),C++编译系统系统自动调用析构函数。
2、多对象的析构顺序
局部对象(后定义先析构) -> 局部的静态(后定义先析构) ->全局对象(后定义先析构)
3、编译器自动生成的默认析构函数
class Date
{
public:
//此时没有进行显示构造函数定义,会使用系统默认生成的无参构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
cout << "调用了析构函数";
}
private:
int _year=1; // 年
int _month=1; // 月
int _day=1; // 日
};
int main()
{
Date d1;
d1.Print();
return 0;
}
输出为:
1-1-1
调用了析构函数
编译器生成的默认析构函数:
对自定类型成员调用它的析构函数。
对内置类型不进行处理。
eg:
class Time
{
public:
~Time()
{
cout << "调用了time的析构函数" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1;
int _month = 1;
int _day = 1;
// 自定义类型
Time _time;
};
int main()
{
Date d1;
return 0;
}
输出为:
调用了time的析构函数
Q:在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
A:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_time是Time类对象,所以在 d销毁时,要将其内部包含的Time类的_time对象销毁,所以要调用Time类的析构函数。但是:main函数 中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数
拷贝构造函数
1、拷贝构造函数定义,特性及使用场景
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用(是构造函数的重载)
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用(&),使用传值方式编译器直接报错,因为会引发无穷递归调用。
-
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝(或值拷贝)。【 在 编译器生成的默认拷贝构造函数中 ,内置类型是 按照字节方式直接拷贝的,而 自定义类型是 调用其拷贝构造函数完成拷贝的】
Q:为什么使用传值方式会引发无穷递归调用?
A:当我们传值调用函数时,
首先传参–>因为是传值会调用新的一个拷贝构造–>新的拷贝构造又要传参–> …
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
#include <iostream>
using namespace std;
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}
// 程序的主函数
int main( )
{
Line line1(10);
Line line2 = line1; // 这里也调用了拷贝构造函数
display(line1);
display(line2);
return 0;
}
输出为:
调用构造函数
调用拷贝构造函数并为指针 ptr 分配内存
调用拷贝构造函数并为指针 ptr 分配内存
line 大小 : 10
释放内存
调用拷贝构造函数并为指针 ptr 分配内存
line 大小 : 10
释放内存
释放内存
释放内存
重载运算符
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 重载双操作数的运算符,第一个参数是左操作数,第二个参数是右操作数
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
可重载运算符/不可重载运算符
可重载的运算符列表:
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),--(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
不可重载的运算符列表:
- . :成员访问运算符
- .* , ->* :成员指针访问运算符
- :: :域运算符
- sizeof :长度运算符
- ?: :条件运算符
- # : 预处理符号
赋值运算符重载
赋值运算符重载语法格式:
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 析构函数
~Date()
{
cout << "~Date()" << endl;//在析构函数内打印,调用一次就打印一次
}
//拷贝构造函数
Date(const Date& d) // Date (Date* this ,Date &d)
// 因为d2调用拷贝构造函数,所以&d2=this, (d是d1的别名)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
// d1=d2 //d1.operator=(&d1,d2)
Date& operator=(const Date& d) // void Date& operator=(&d1,const Date& d)
{
if (this != &d) // 对d取地址,判断this的值和d的地址是否相同,如果不是自己给自己赋值,才需要拷
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
};
int main()
{
Date d1(2024, 2, 28);
d1.Print();
Date d2;
d2.Print();
d2 = d1;
d2.Print();
return 0;
}
输出为:
2024/2/28
2024/1/1
2024/2/28 说明 赋值运算符重载 成功
~Date()
~Date()
前置++和后置++重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
Date& operator++()
{
(*this) += 1;
return *this;// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
}
// 后置++:
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
Date operator++(int)// 前置++和后置++都是一元运算符,加个int为了让前置++与后置++形成能正确重载
{
Date temp(*this);
(*this) += 1;
return temp;//temp是临时对象,因此只能以值的方式返回,不能返回引用
}
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
private:
int _year;
int _month;
int _day;
};
前置递增运算符++的重载函数返回的是Date对象的引用,因为前置递增运算符会先对对象进行加一操作,然后返回加一后的对象本身,因此返回的是引用。这样可以实现连续的递增操作。
后置递增运算符++的重载函数多增加了一个int类型的参数(虽然在调用时不需要传递),这是为了与前置递增运算符形成重载。在函数内部,先将当前对象的值保存到临时对象temp中,然后对当前对象进行加一操作,最后返回保存了旧值的临时对象temp。这样可以实现先返回旧值再进行递增的语义
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const//参数是const Date* this,因为保证*this不变(内容不变,指向可以变)
{
return this;//返回const Date* 类型
}
private:
int _year;
int _month;
int _day;
};
- 对于非const对象,我们返回指向对象的指针;
- 对于const对象,我们返回指向const对象的指针。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
初始化列表
概念:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 1)//使用全缺省,也是默认构造函数
:_year(year)
,_month(month)
,_day(day) //初始化列表
{
}
private:
int _year;
int _month;
int _day;
};
特性:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
解决的问题:
- 必须在定义的地方显示地初始化:引用 const
- 没有默认构造函数的自定义成员
- 有些自定义成员想要自己控制自己的初始化
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以能不用就不用。
友元包括:友元函数和友元类
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要在前面加friend关键字
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
注意:
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递。
- 友元关系不能继承。
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth(Box box);
friend class BigBox;
void setWidth(double wid);
};
class BigBox
{
public:
void Print(int width, Box& box)
{
// BigBox是Box的友元类,它可以直接访问Box类的任何成员
box.setWidth(width);
cout << "Width of box : " << box.width << endl;
}
};
// 成员函数定义
void Box::setWidth(double wid)
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}
// 程序的主函数
int main()
{
Box box;
BigBox big;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth(box);
// 使用友元类中的方法设置宽度
big.Print(20, box);
getchar();
return 0;
}
输出为:
Width of box : 10
Width of box : 20
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
注意:
- 外部类对内部类没有任何优越的访问权限
- 内部类的构造函数不能直接初始化外部类的对象。如果需要在内部类中使用外部类的对象,应该使用指针或者引用。
class A
{
public:
A(int a = 0)
:_a(a)
{ }
class B//B这个内部类是A的友元
{
public:
void print(A& _ra)//通过引用或者指针
{
cout << _b << endl;
cout << _ra._a << endl;//访问外部类的私有变量
}
private:
int _b;
};
private:
int _a;
};
其实B就是一个普通类,只是受A的类域和访问限定符限制,本质相当于被封装了一下
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系
匿名对象
在C++中,可以创建匿名对象,即在创建对象的同时不给对象命名。匿名对象通常用于临时使用,无需在其他地方引用该对象。
注意:
匿名对象的生存周期仗赖于所在作用域,在离开作用域时会自动被销毁。因此,应当避免在需要长时间使用的情况下过度依赖匿名对象。(生命周期只在当前行!!!)
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "MyClass 构造函数被调用" << endl;
}
~MyClass() {
cout << "MyClass 析构函数被调用" << endl;
}
void display() {
cout << "display 函数被调用" << endl;
}
};
int main() {
// 创建匿名对象并调用成员函数
MyClass().display();
return 0;
}
输出为:
MyClass 构造函数被调用
display 函数被调用
MyClass 析构函数被调用