前言
本文是紧接着上一篇《初识CPlusPlus》的后续内容,当然也不是最后,只是现阶段整理出来的就这么多。写的也回更加详细点,讲出作用意义。
本文,将介绍C++中的引用、函数模版、inline函数、类和对象及其中具体内容,包括构造函数、析构函数等6大函数,拓展的2种函数留到之后再讲。
一、引用
1、简介
引用就是祖师爷觉得C语言中的指针太麻烦了,每次使用都需要解引用“*”。所以直接让计算机帮你完成了解引用的操作,这就是引用的本质。
实际上使用引用就像是取别名一样,像《水浒传》中的豹子头林冲那样,取得别名。在日常生活中就像小名一样,反正就是叫的同一个人。
2、举例说明
2.1、引用的使用方式
我们可以定义一个整形变量a,然后把它重命名为c,b,也可以把这里的b引用给d。这样上述4个变量其实都是同一个变量:
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = b;
cout << "a = " << a << " &a = " << &a << endl;
cout << "b = " << b << " &b = " << &b << endl;
cout << "c = " << c << " &c = " << &c << endl;
cout << "d = " << d << " &d = " << &d << endl;
return 0;
}
2.2、引用和指针的区别
这里的区别就简单提一嘴,引用虽然是指针,但是引用不能像指针那样改变指向。因此引用必须在最开始的时候就给出引用的对象。以下写法没有给出对象是错误的:
int& b;
和指针的相同点,就是存在着权限的问题。引用的时候不能放大权限,在指针的时候我们知道给const变量定义指针的时候需要用const修饰指针指向的变量,例如:
int main()
{
const int a = 10;
const int* b = &a;
return 0;
}
对于引用来说也是相同的,例如:
int main()
{
const int a = 10;
const int* b = &a;
const int& c = a;
const int& d = 10;
return 0;
}
常量也是可以引用的,但是不能修改,需要const修饰。
2.3、函数与引用
函数中也可以使用引用,它的效果和指针类似。用法也类似,需要注意的点也大差不差。实际上是指针,所以函数参数和返回值都能够使用引用进行传递。但也要注意不要出现以下状况:如果传递回去的返回值是需要在函数中销毁的(将亡值),那么就不能使用引用返回。
int& func()
{
int a = 0
return a;
}
因为出了函数作用域,函数内定义的局部变量会销毁,原来存值的空间可能会被其他程序使用,那么返回值会出错。除此之外还要注意引用传值的修改会影响所有值,和指针相同。因为指向的位置是同一个空间。
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = b;
d = 20;
cout << "a = " << a << " &a = " << &a << endl;
cout << "b = " << b << " &b = " << &b << endl;
cout << "c = " << c << " &c = " << &c << endl;
cout << "d = " << d << " &d = " << &d << endl;
return 0;
}
所以函数中需要注意,虽然引用和指针一样会减少空间的使用,但也要注意,如果使用过程中不需要修改值的话,最好加cosnt修饰,以免程序出错。
二、函数模版
函数模板定义的一般形式如下:
template<类型形式参数表>
返回类型 函数名(形式参数表)
{
... //函数体
}
它的意义在于减少了重复劳动,交给计算机编写函数,例如之前写的函数:
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
现在只需要利用函数模版就能解决:
template <class T>
T Add(T a, T b)
{
return a + b;
}
这样的情况下不仅包含了浮点数、整数,其他的类型也会按照模版自动推算。另一方面,模版也可以具体化,那么函数会优先调用最接近的。这个举例之后重新学到的时候会仔细讲,这里只是浅提一嘴。
三、inline修饰的函数
1、简介
宏函数,想必大家都清楚。优点是不需要想函数那样栈针,而是编译的时候直接替换。缺点则是写起来需要加括号明确优先级、并且不好调试。为了解决这样的问题,就有了inline函数。效果是能够让函数不栈针,直接在对应位置展开。在汇编里面就相当于不会看到call。
需要注意的是,inline修饰的函数只是向编译器建议直接展开。如果函数太长,或者其他的原因可能在编译过后还是向普通函数那样执行。
2、特点
inline修饰的函数也被称为内联函数,内联函数不会展开,不建立栈帧:
(1)inline对于计算机来说是建议不建立栈帧,是建议。
(2)宏函数也不需要展开,这里出现inline就是为了替代宏函数。
(3)debug版本下默认不展开inline方便调试。
(4)内联函数不建议声明和定义分离到两个文件,分离可能会导致链接错误,因为inline不被展开,就不会去找函数的地址,链接可能出错。
3、使用举例
函数前面增加inline即可。
inline void func(int a, int b){}
四、类和对象
1、简介
类和对象是C++出现的,如果说C语言是面向过程的,那么因为类和对象,C++变成了面向对象的语言。
类的使用和结构体类似,不过在C++中的类和结构体能够包括函数,也就是说能够更方便的找到对应结构体的操作函数。除此之外还优化了C语言中,结构体的名字需要增加“struct”的问题。在C++中,这个关键字只需要出现在建立结构体的时候就可以了。
类的关键字是“class”。
2、访问限定字符
为了改进C语言中容易随意修改结构体成员,C++中引入了3整访问限定符:protect、private、public。
protect:保护,能够保护成员、该成员无法直接调用、可以被继承。
private:私有,能够保护成员、该成员无法直接调用且该成员无法被继承。
public:公有,能够被随意调用,可以被继承。
在类中如果不写这些关键字,那么默认是私有。结构体中默认是公有。
使用方法,关键字加“:”从此之后都表示是同一类型,直到遇到另外的关键字,例如:
class Exp
{
public:
void func()
{
cout << "func()" << endl;
}
private:
int a;
int b;
public:
double d;
};
在这个类中函数“func()”和“d”都是公有的,而变量“a”和“b”是私有的。
那么接下来来介绍访问的区别:
int main()
{
Exp exp; // 也可以写作class Exp
// exp.a;
// exp.b;
// 对于 a 和 b 因为是私有无法直接访问,只能通过类里面的公共函数,或者友缘函数访问
exp.d = 3.33;
exp.func();
// 而变量 d 和 函数 func()是可以直接访问,变量d也可以修改
return 0;
}
3、类的大小
类的大小和结构体大小的计算方式相同,只需要看变量的大小。类里面的函数不存在于类中,而是在静态区。
4、this指针
this指针就相当于指向这个结构体的指针,可以直接在结构体中使用。例如在用函数去成员变量的时候:
class Exp2
{
public:
void func()
{
_a = 1;
_b = 2;
cout << _a << endl;
}
private:
int _a;
int _b;
};
如上结构体所示,函数中取了该类中的变量,那么这个函数是怎么找到这些变量的呢?实际上是计算机会自动添加“this->”帮你取到该类的成员,这样才不会取到其他地方。如果手动添加了那么计算机就不会继续添加。
该类的函数实际的样貌是:
void func()
{
this->_a = 1;
this->_b = 2;
cout << this->_a << endl;
}
5、类的默认访问成员函数
在C++98的时候有6种编译器会默认生成的成员函数。例如:构造函数、析构函数、拷贝构造、赋值重载等等。
接下来就简单介绍一下这4种函数:
5.1、构造函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。
特点:
(1)构造函数名字于类的名字相同
(2)无返回值,void也不行
(3)对实例化对象自动调用
(4)不手动写计算机就会自动生成,但是如果需要申请空间,就需要自己写
(5)构造函数能够有重载
(6)默认构造函数只有一个,就是没有形参的那个。
(7)默认生成的构造函数对初始化没有要求,所以构造函数中如果有自定义类型或者指针建议自行写。
使用举例:
class Exp3
{
public:
Exp3(int a, int b)
:_a(a) // 这种赋值方法是c++中增加的在变量后加括号,表示调用它的构造
,_b(b) // 所以通过这种方式也能够初始化内置类型
{}
Exp3() // 构成重载
{
_a = 1;
_b = 2;
}
private:
int _a;
int _b;
};
默认构造函数的调用是在如下情况,没有赋值的直接生成:
int main()
{
Exp3 exp;
return 0;
}
也可以通过构造函数直接对成员变量进行赋值,使用方法如下:
int main()
{
Exp3 exp(3, 4);
return 0;
}
5.2、析构函数
与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。以C++语言为例:析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
特点:
(1)析构函数在类名前增加~
(2)无参数、无返回值
(3)一个类只有一个析构函数,没有会自动生成
(4)生命周期结束会自动调用,就是出了函数就析构
(5)析构时,自定义的类型会调用自己的析构函数,且这个过程会有计算机帮助完成,故不需要写,内置类型不做处理。
(6)自定义的类型会调用自己的析构函数
(7)析构的顺序和栈类似,后定义的先析构。
使用举例:
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
~B()
{
cout << "~B()" << endl;
}
};
class C
{
public:
~C()
{
cout << "~C()" << endl;
}
private:
int _c;
A _a;
B _b;
};
void func()
{
C c;
}
int main()
{
A a;
func();
B b;
return 0;
}
在以上例子中,生成了a、b、c三个变量,其中c先到生命周期——到创建它的函数的结尾,所以会先析构。然后到main()函数结束,会先析构后创建的变量b,最后析构a。
5.3、拷贝构造
每一个类只有一个析构函数,但可以有多个构造函数(包含一个默认构造函数,一个拷贝构造函数,和其他普通构造函数)和多个赋值函数(包含一个拷贝赋值函数,其他的为普通赋值函数)。
特点:
(1)拷贝构造名字于类的名字相同
(2)无返回值,void也不行
(3)对实例化对象自动调用
(4)不手动写计算机就会自动生成,但是如果需要申请空间,就需要自己写
(5)拷贝构造有且只有一个形参,而且形参需要增加&符号。
使用举例:
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 0, int day = 0)
:_year(year)
,_month(month)
,_day(day)
{}
// 拷贝构造
Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
// 内置类型,析构可以机器自动生成
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 11);
Date d2(d1); // 拷贝构造的第一种用法
Date d3(1921, 7, 23);
d1.Print();
d2.Print();
cout << endl;
d1 = d3; // 拷贝构造的第二种用法
d1.Print();
return 0;
}
5.4、赋值重载
操作符重载,计算机学科概念,就是把已经定义的、有一定功能的操作符进行重新定义,来完成更为细致具体的运算等功能。操作符重载可以将概括性的抽象操作符具体化,便于外部调用而无需知晓内部具体运算过程。
例如我们使用流插入、流提取的时候,用的就是操作符重载。将<<重载为输出,将>>重载为输入。原来在C语言中表示移位的操作符就有了另外的作用。
用法是operator加上重载的符号。
使用举例:
例如我们需要比较日期的大小,那么年月日可能都要比较到,所以需要用到赋值重载。
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 0, int day = 0)
:_year(year)
,_month(month)
,_day(day)
{}
// 拷贝构造
Date(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
// 内置类型,析构可以机器自动生成
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
bool operator>(const Date& date)
{
if(_year > date._year)
{
return true;
}
else if(_year == date._year)
{
if(_month > date._month)
{
return true;
}
else if(_month == date._month)
{
if(_day > date._day)
{
return true;
}
}
}
return false;
}
bool operator<=(const Date& date)
{
return !(operator>(date));
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 11);
Date d2(d1);
Date d3(1921, 7, 23);
cout << (d1 > d3) << endl;
cout << (d1 <= d2) << endl;
return 0;
}
另外+、-、*、/等符号都可用赋值重载进行操作符重载。
这一类重载的函数反而最多,剩下的交给读者自行探索。
作者结语
这一次内容还挺多的,整理了一下也还是写了好多,其中的引用、函数模版、类的用处很多很广,学到这里我们也就刚刚入门了。大佬的代码估计还是看不太懂。毕竟C++有太多的封装嵌套,然后还有各种离谱操作。
学吧,啥时候整到无敌的水平,就能够靠编程在这个世界横着走。我愿称此为编程界,愿大家早日得道成尊。
ps:“没有乐土大人,我们如何抗衡双尊”,方源哈哈大笑“这简单,我成尊不就是了?”