对比C的改进
一、命名空间
用c语言进行合作编程时难免会出现命名重复的情况,特别是函数名。而c++的命名空间很好的解决了这一问题。总的来说,命名空间就是防止出现命名重复而开辟处理存储变量名和函数的空间。
如图:函数名同样是叫做fun,但通过不同的命名空间将其分隔,访问时只需要指定命名域就可以访问到想要的函数。
访问顺序
如果不指定命名域,那么就先局部域再到全局域;指定的话就到指定域寻找。
注:这也可以解释为什么在c语言,使用变量时优先使用局部变量的原因。
访问方法
命名空间::函数名 命名空间::变量名
为什么注释掉using namespace std;之后下面就报错了呢???
在头文件iostream中有很多命名空间,using namespace std;的意思是展开std这个命名空间。展开后就可以直接写std命名空间中的名字,而不需要std::cout<<"text"<<std::endl。当然,这样仅仅是便于平时练习,不然都展开命名空间,那么命名空间又有什么意义,那该重复还是重复。
命名空间可以嵌套
其实嵌套时调用就是多套一层指定域,text1::text2::fun()。但一般来说不会嵌套很多层,毕竟一是够用,二是用的时候也麻烦。
二、缺省参数
缺省参数,其实就是默认参数,当调用函数时不输入参数就使用这个缺省值作为该形参的值,当然如果有输入则使用输入的值,这样一来函数可以更加灵活。
下图没有为形参b输入参数,所以使用缺省值1。
下图为b输入了参数,使用优先使用输入的参数,不使用缺省值。
当然也可以写成全缺省参数的函数,一些初始化函数这样写,调用时参数可输可不输。或者输入一部分参数也行,要注意的是输入的实参和形参是一一对应的。(第一个实参对应第一个形参,第二个对应第二个......)
注意事项
- 缺省参数得放在右边且必须连续。(或者全缺省,不然就如下图所示)
- 若是声明与定义分离,则缺省参数一般写在声明处。(如果声明和定义位置同时出现缺省运行时会报错“重定义默认函数”)
- 缺省值必须是常量或者全局变量
三、函数重载
其实在C++中是可以存在功能类似名字相同的函数,但是其参数不能一样。
1.参数可以是类型不同
2.参数可以是参数的个数不同
3.参数可以是参数的类型顺序不同
函数名相同,参数不同。这样的情况我们称为函数重载 。
为什么C++支持重载?
答案是和编译过程有关,C语言在汇编过程中产生的符号表中的函数名是没有修饰的,若是同名编译器无法分辨是哪一个函数。而C++通过函数修饰规则来区分这些同名但参数不同的函数(简单地说就是因为修饰规则的存在,参数的类型和顺序会影响函数最终的名字)
四、模板
认识了函数重载就不得不说一下模板,重载的函数往往是功能相似的,又或者说是代码相似度高。如果要考虑所以重载的情况写的重复代码会非常得多,而模板解决了这个问题。
关键字 template
使用时会像这样去创建类型
template <类型形式参数表>
类型形式参数表可以有多个类型也可以只有一个类型。
如下图,sum在这里作为一个模板,而T是同一种类型,当输入两个整形时编译器就会按照模板以及类型形式参数表生成一个返回值,a,b都为整形的函数(int sum(int a, int b)),输入两个double类型的参数就会生成一个返回值,a,b都为double的函数(double sum(double a, double b))
当类型形式参数表中的类型不止一个时写法如下:
这个时候会不会下意识认为T和T2一定是不同的类型?
第一组数据调试,不难看出T是int,T2是double
第二组数据调试,T是int,T2也是int。所以模板中使用的类型个数仅代表最多能容纳多少种类型,但实际上调用时却不一定这么多种类型
注意事项
一个关键字后面跟一个模板,不可以一个关键字跟多个函数模板
当存在符合条件的函数编译器优先使用写好的函数(用模板生成函数消耗性能,所以有符合条件的函数就不生成了)下图是编译器逐语句调试的结果
五、引用
引用就是变量的别名,本质上是同一个变量。
举个简单的例子:
a他有个别名叫做b,所以b++可不就是a++嘛。
引用有什么用?
C语言中的传值调用相信大家不陌生吧,他不是真的把值传过去,而是传了一份临时拷贝。但是,拷贝是要浪费性能的,要额外开空间的。而引用作用就是提高函数效率,节约空间。(另外当指针过于复杂,用引用来辅助编写代码也不错)
引用的特点
- 引用必须在创建时被初始化,所以不存在空引用,他必须连接一块合法空间。
- 其次引用一旦确定是谁的别名,就无法修改(不像指针可以改变指向)。
权限问题
在这里a是不可修改的,是个常变量。但是给了b之后却能改了。权限被放大了(这是不可行的)
权限可以平移
权限可以缩小
临时变量
(变量是左值,常量是右值)这里的a+b实际上产生了临时变量,而临时变量我们默认为不可修改(被const修饰) ,所以说“非常量引用的初始值必须是左值”,因为权限被放大了。
临时变量的产生情况
- 值传递时会产生(例如函数返回 返回值时会产生;传递实参给函数时会产生。)
- 类型转换时会产生(包括强制类型转换)
六、内联函数
函数有几大优点,一可以不用重复编写某些使用率高的功能;二更有逻辑性使代码有更好的可读性;三确保程序的行为统一,便于调试;四修改时不需要处处修改。但是调用函数比运行等价表达式要慢得多,毕竟函数调用是要建立栈帧和销毁栈帧的。
关键字 inline
关键字 inline 放在函数定义的前面,即刻将函数指定为内联函数。
对于内联函数来说,主要是省去了建立栈帧和销毁栈帧的消耗,等于在调用函数的地方运行等价的表达式。由于函数重载和内联函数的存在,宏在C++的意义也不大了。
注意事项
- 声明定义分离的情况下,关键字inline仅放在声明处无法构成内敛函数,一定要在定义前也加上关键字inline。
- 定义在类声明内中的成员函数默认是inline函数。
- 内联函数中不允许使用循环语句和开关语句(switch),如果有这些语句则编译器会将其视作普通的函数。
- 递归函数不可作为内联函数。
类与对象
类是C++的自定义类型,是用于指定对象的形式。其作用像是一个模板,可以根据模板实例化出对应属性的对象。
一、成员与对象
在类中的数据称为成员变量,类中的函数叫成员函数。
//日期类
class Date
{
//成员函数
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDays[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 monthDays[month];
}
//成员变量
private:
int _year;
int _month;
int _day;
};
在类中的成员可以被赋予 私有 公有 保护 三种属性中的一种。
- 公有(关键字 public):类内和类外都可访问
- 私有(关键字 private): 友元(友元下面内容会介绍)、类内可访问,类外不可访问。
- 保护(关键字 protected):不涉及继承的情况下与私有无异()
实例化对象以及函数调用
class A
{
public:
void Printf()
{
//类中的函数调用私有成员_num
cout << _num << endl;
}
private:
int _num = 2333;
};
int main()
{
//这里是以 类A 为类型创建的对象 a
A a;
//通过对象a去调用公有函数Printf
a.Printf();
return 0;
}
实例化对象的方法 | 类名 变量名 (以叫 类名 的类为模板创建一个叫 变量名 的变量) |
对象调用函数 | 对象名.成员函数名(参数) |
如下图,对象a和对象b分别调用函数Printf的结果(a中的_num被初始化为1,b中的_num被初始化为2,不用太在意(1)、(2)和函数A,在构造函数处会详细介绍)
同样是调用Printf函数,又没有参数传入编译器是怎么分辨他们的值?
其实用对象去调用函数时就已经传入了一个隐藏参数,就是指向该对象的指针,这个指针叫做this。展开一看就简单了(如下图)。看,当我们输入this->时弹出了可以指向的成员(当然一般不需要将这个指针写出来,编译器会默认加上)。
const修饰成员函数
首先与以往不同的是,const要写在函数的小括号后面,并且const修饰的是this指针。一般我们不希望对象的值被修改,就可以用const修饰成员函数。
class A
{
public:
void Printf() const
{
cout << _num << endl;
}
private:
int _num = 2333;
};
成员函数的声明与定义分离
要在函数名前加上 类名:: 以指定是谁的GetMonthDay函数
Date.h文件中
//日期类的声明
class Date
{
//成员函数
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
//成员变量
private:
int _year;
int _month;
int _day;
};Date.c文件中
//要在函数名前加上 类名:: 以指定是谁的GetMonthDay函数
int Date::GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDays[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 monthDays[month];
}
注意事项
- 既然定义了一个类,那么我们应该是想要用这个类创建的对象去管理一些数据,所以成员变量一般设为私有,防止在外部被直接更改。
- 实例化的对象只会开成员变量的空间,至于成员函数存储在一个公共代码区。(如果每一个对象都存储函数就太浪费空间了)
- 类是可以嵌套的
//类的嵌套
class C
{
public:
void Fun()
{
cout << "c" << endl;
}
private:
int _c;
};
class A
{
public:
A(int num = 0)
{
_num = num;
}
void Printf()
{
cout << this->_num << endl;
}
class B
{
public:
void Fun()
{
cout << "b" << endl;
}
private:
int _b;
};
private:
int _num;
C c;
};
二、友元
对于私有的成员变量是无法在类外访问的,但是如果在类中声明了友元的函数或类就可以在友元中去访问该类的私有。
友元函数:Printf函数是一个全局函数,但在类A中声明了是友元,所以可以直接访问私有。
友元类:类B是类A朋友,所以在类B中可以直接访问类A的私有。
友元类偶尔会出现塑料朋友的情况,只能说扎心了老铁。所以,当需要互相能够访问私有时,得双方都声明友元。同时要搞清楚谁是谁的朋友,谁可以访问私有。
三、静态参数与静态函数
被static修饰的成员变量称为静态成员变量,静态成员数量是类域中的全局变量。静态成员变量可以实现对象间的数据共享。
静态成员变量
静态成员变量的特点:
- 必须被初始化,且一般在类外初始化。
- 静态成员变量属于类,不独属于某个对象。因此静态成员变量不占用对象内存,在所有对象外开辟内存,即使不创建对象也可以访问。
- 编译时在静态数据区分配内存,到程序结束时才释放。(普通成员变量在对象创建时分配内存,在对象销毁时释放内存。)
- 静态成员变量初始化不赋值会被默认初始化,通常是0。
- 静态成员变量可以成为成员函数的可选参数,而普通成员不行
静态成员变量的特点第五条,为什么普通成员函数不行?
普通成员函数得有this指针才可以访问,而如图所示,该指针只能用于非静态成员函数内部。
但静态成员函数不需要this就可以访问,因为他属于类,不属于对象。
class A
{
public:
//每调用一次Printf函数count就加1
void Printf()
{
count++;
cout << count << endl;
}
private:
int _num;
//定义一个静态成员变量
static int count;
};
//初始化为0
int A::count = 0;
int main()
{
A a;
a.Printf();
A b;
b.Printf();
return 0;
}
运行结果:
为什么运行的结果是1 2?
根据静态成员变量特点第二条:静态成员变量属于类,不独属于某个对象。因此静态成员变量不占用对象内存,在所有对象外开辟内存,即使不创建对象也可以访问。所以,实际上对象a和对象b都共用一个静态成员变量,a调用完count就等于1,轮到b调用就变成2了。
静态成员函数
普通成员函数能调用所有的成员变量,而静态成员函数只能调用静态成员变量。因为静态成员函数与对象无关,所以没有this指针。
静态成员函数的用处:
与类实例(对象)无关,所以即使没有对象也可以调用。
可以把类名当成namespace(命名域)用。
控制该函数的访问权限(不能访问普通成员变量)。
控制类内的static变量。
四、类的六大默认成员函数
类的六大默认成员函数 |
---|
1.构造函数(用于初始化对象) |
2.析构函数(用于销毁对象,回收内存) |
3.拷贝构造函数(同为构造,但用对象作参数来从初始化) |
4.赋值操作符重载(将需要的操作符重载,使其适用于该类) |
5.取地址运算符重载 |
6.const取地址运算符重载(不希望地址被改变时使用) |
构造函数
构造函数会在创建对象时自动调用,对该对象进行初始化。
- 构造函数的名字要与类名一致
- 如果不编写编译器会自动生成一个默认构造函数(但这个生成的函数什么都不会做)
- 如果编写了构造函数,无论其是不是默认构造函数,编译器都不会再生成
- 至少要存在一个默认构造函数,从而保证初始化有合适的构造函数
- 不写返回值(构造函数不能定义返回类型)
- 如果成员变量中有自定义的类类型,会自动调用该类型的构造函数。
下面是常见的几种写法
class A
{
public:
//初始化方法固定,不推荐
//创建对象的使用方法:A a;
A()
{
_num = 1;
}
//形参全是缺省参数,可传参可不传,推荐使用
A(int num = 1)
{
_num = num;
}
//必须传参,限制较大
//创建对象的使用方法:A a(1);
A(int num)
{
_num = num;
}
private:
int _num;
};
全缺省的构造函数创建对象的使用方法:
默认构造函数
默认构造函数是指无参数调用的构造函数
默认构造函数分为以下三种:
- 没有参数的构造函数 A()
- 参数均为缺省参数的构造函数(因为它可以不传参) A(int num = 1)
- 没有编写,编译器自动生成的默认构造函数
拷贝构造
拷贝构造也是构造函数的一种,不过是以对象为参数进行初始化。
class A
{
public:
//将对象a的数据拷贝一份给当前对象(即this指针指向的对象)
A(const A& a)
{
_num = a._num;
}
private:
int _num;
};
使用方法:
可能有人注意到了,拷贝构造的参数是一个引用,直写 const A a 作为参数是否可行?
答案是不行!这个就因为传值调用的传参是拷贝一个临时变量,而拷贝对象就会调用拷贝构造,如果不使用引用就会导致无限递归。
初始化列表
构造函数除了名字、参数列表、函数体,还可以有初始化列表。(注:初始化不等于直接赋值)
初始化列表写法:在参数列表后以冒号开始 成员变量名(初始化值) 并以逗号分隔,之后才是函数体。
class Date
{
public:
//初始化列表
Date(int year = 2000, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
//函数体
}
private:
int _year;
int _month;
int _day;
};
必须使用初始化列表的三种情况:
- 常量成员(常量只能初始化不能进行赋值)
- 引用类型(引用定义的时就必须初始化,确定是谁的别名后就不能改变)
- 没有默认构造函数的类(初始化列表不一定非要调用默认构造函数来初始化,可以直接调用拷贝构造函数来初始化 )
初始化成员变量的顺序:
即使在初始化列表处将 _month 放在 _year 的前面,也会先初始化 _year。
因为初始化的先后顺序是与成员变量定义的顺序一致的,所以一般我们会将两者顺序保持一致,以免出现错误。
class Date
{
public:
//初始化列表
Date(int year = 2000, int month = 1, int day = 1)
: _month(month)
, _year(year)
, _day(day)
{
//函数体
}
private:
int _year;
int _month;
int _day;
};
如下图,如果顺序不一致,有时候难免会因为赋值顺序出现赋值问题。因为先初始化a1,但a2还没初始化所以还是随机值,导致a1初始化后变成随机值。
class Test
{
public:
//初始化列表
Test()
: a2(1)
, a1(a2)
{
//函数体
}
void Printf()
{
cout<< a1 << a2 <<endl;
}
private:
int a1;
int a2;
};
int main()
{
Test t;
t.Printf();
return 0;
}
析构函数
有用于初始化的构造函数,就有用于回收资源的析构函数。
析构函数特点:
- 名字同样与类名一致,但要在名字前加上 ~ 符号。
- 在对象的生命周期结束时自动调用(可以一定程度避免内存泄漏)
- 不进行编写编译器同样自动生成(但同样什么都不干)
- 不接受自变量,也不返回值。 同时无法声明为 const 、volatile 或 static
- 如果成员变量中有自定义的类类型,会自动调用该类型的析构函数。
析构函数对于那些只有内置类型的类意义不大,
class A
{
public:
//构造函数
A(int num = 1)
{
_num = num;
arr = (int*)calloc(sizeof(int),_num);
}
//析构函数
~A()
{
free(arr);
arr = NULL;
}
private:
int _num;
int* arr;
};
运算符重载
运算符重载是一种特殊的函数重载,使运算符可以应用在类类型上。
返回类型 operator要重载的运算符 (参数列表)
{
函数体;
}
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date Date::operator+(int day)
{
//拷贝构造
Date tmp = *this;
tmp += day;
//返回tmp的临时拷贝
return tmp;
}
后置++和前置++重载的区别(--同理):
为了区别前置++和后置++,规定上来说要在后置++的参数列表加上一个参数类型形成重载。又因为实际上不需要接收参数所以不需要写出变量名,仅写类型形成重载。
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
取地址运算符重载
//取地址运算符重载
Date* Date::operator&()
{
return this;
}
//const取地址运算符重载
const Date* Date::operator&()const
{
return this;
}