定义:是一个变量的别名,不占用内存空间;只能作为一个变量的附属存在。
格式:存储类型& 引用名 = 变量名;
&的含义取决于其所在到的位置
指针是一种底层机制,引用是一种较高层的机制,从语言的概念来说,引用是变量的一个别名,实际在使用引用的时候,依然需要地址。所以引用只不过将地址这一概念隐藏起来了,方便开发者使用。
引用必须初始化
因为其不占内存空间,是变量的附属,所以其在定义的时候必须初始化。
int& p; //报错
//错误详情:
// “a”: 必须初始化引用
变量的引用无法引用常量
因为变量引用使用的时候就代表这他所属的变量本身,其在权限上是可读可更改,若使用变量的引用引用常量则表示可以通过变量的引用更改常量,但是常量是不可更改的。所以其变量的引用无法引用常量。
int& p = 4; //错误
//错误详情:
// “初始化”: 无法从“int”转换为“int &”
变量的引用没有类型兼容一说
强制类型转换也不可以,当然,C++的强制类型转换和C语言的强制类型转换会有所不同。
char a = 'a';
int& pa = a; //错误
//错误类型:
// “初始化”:无法从“char”转换为“int&”
char a = 'a';
int& pa = (int)a; //错误
//错误类型:
// “初始化”:无法从“int”转换为“int&”
变量的引用在初始化之后,不能再引用其他的变量
其能且仅能在初始化的时候引用变量。
int b = 10;
int c = 20;
int& pb = b;
pb = c;
cout << b << endl; //运行结果为20
//此过程并无语法错误,但是无法做到使pb引用其他变量的目的
//此过程是为b变量赋值的过程
引用作为实参
引用作为实参传递时同直接使用变量作为实参并无区别
void fun(int a)
{
a++;
}
int main()
{
int a = 10;
int& pa = a;
fun(pa);
cout << a << endl;
}
//程序正常运行,输出结果为:10
引用作为形参
引用作为形参时,类似于地址传递,可以通过形参修改实参的值。
void fun(int& a)
{
a++;
}
int main()
{
int a = 10;
fun(a);
cout << a << endl;
}
//程序正常运行,输出结果为:11
错误情况大概分为三种:
使用变量接收函数返回的局部变量的引用
如下函数,a是一个局部变量,当fun函数执行完毕时,会被出栈释放,但是,函数返回值将其的引用作为返回值返回,这意味着返回的是一块未知空间的引用,这是不安全的。
int& fun()
{
int a = 100;
return a; //出现警告
//警告详情:返回局部变量或临时变量的地址: a
}
int main()
{
int b = fun();
cout << b << endl;
}
使用引用接收函数返回的局部变量的引用
如下函数种的操作极其危险,上种情况仅仅是访问未知空间,但是在本函数中,由于fun函数返回的是引用,而主函数中又是使用引用接收,意味着主函数中的b是一块未知空间的别名,我们可以对其进行修改。这是极其危险的。
int& fun(void)
{
int a = 100;
return a; //警告
//警告详情:返回局部变量或临时变量的地址: a
}
int main()
{
int& b = fun();
cout << b << endl; //输出为100
b = 200;
cout << b << endl; //输出为200
}
使用引用接收函数返回的局部变量
此种写法是错误的,编译都无法通过。
int fun(void)
{
int a = 100;
return a;
}
int main()
{
int& b = fun();//错误
//错误详情:“初始化”: 无法从“int”转换为“int &”
}
错误原因和局部变量返回值的处理过程有关。当一个函数执行完毕后会进行出栈,其返回值会被保存在一块临时空间,而这块空间是归于内核的,我们无法引用。
错误总结:
在函数返回的引用有意义的情况下,函数引用可以作为左值
该类型主要用于关闭判断,有且仅有两个值:true、false
大小是一个字节
布尔类型变量可以被赋值整数(只会出现警告,并不会出现错误)
#include
int main()
{
bool b = false; //正确运行,但有警告
//警告详情: “=”: 从“int”到“bool”截断
b = 10;
int a = 10;
b = a; //正常运行,但有警告
//警告详情:“int”: 将值强制为布尔值“true”或“false”(性能警告)
return 0;
}
C语言中,有两个及两个以上的函数同名,则程序编译失败。这种方式会导致很多功能相似的函数会有不同的函数名,很混乱。
C++中使用函数重载来解决这个问题。
多个同名函数,拥有不同数量或者不同类型或顺序不同的形参,即可构成函数重载。编译器会根据形参来进行匹配调用函数。
注:返回值不同和、不会构成函数重载
int add(int a, int b)
{
return a + b;
}
double add(int a, int b)//错误
{ //错误详情:“double add(int,int)”: 重载函数与“int add(int,int)”只是在返回类型上不同
//“add”: 重定义;不同的基类型
return (double)a + b;
}
double add(double a, int b)//正确
{
return b + a;
}
double add(double a, double b)//正确
{
return a + b;
}
double add(int a, double b)//正确
{
return a + b;
}
int add(int a, int b, int c)//正确
{
return a + b + c;
}
函数在定义的时候允许形参被赋予默认值。
当我们进行从、函数调用时,若不为该位置传递实参,则该位置的会使用默认值来初始化。若我们提供实参,则会利用我们提供的实参来初始化。
默认参数可以是常量、常量表达式、变量
int g_a = 10;
// 变量 常量 常量表达式
int add(int a = g_a, int b = 10, int c = 10+30*2)
{
return a + b + c;
}
默认参数的位置
默认参数的右边不能有非默认参数
int add(int a, int b = 10, int c)//错误
{ //错误详情: “add”: 缺少参数 3 的默认参数
return a + b + c;
}
//此种写法,若只想填写第一个个第三个参数是做不到的,此时,默认参数失去了意义。
int add(int a, int b, int c = 10)//正确
{
return a + b + c;
}
int main()
{
add(20, 30);//正确
return 0;
}
默认参数和函数重载
避免二义性问题
如下情况中,两个add函数应为参数数量不同构成函数重载,但是由于三个参数的add函数有一个默认参数,所以调用时可以仅填写两个参数,此时,编译器不知该调用哪个版本的函数。
int add(int a, int b, int c = 10)
{
return a + b + c;
}
int add(int a, int b)
{
return a + b;
}
int main()
{
int a = 20, b = 30;
add(a, b); //错误
//错误详情:“add”: 对重载函数的调用不明确
return 0;
}
引用和充当默认参数
引用可以充当函数参数,由于引用的性质,其只能引用变量,一般是全局变量等,此时要确定所引用的变量的变化时能够被程序员控制的。一般不建议函数参数是引用时带有默认参数。
默认参数和函数声明
当函数需要函数声明时,默认参数只能填写在声明处,且函数定义处不能写默认参数,这个程序的编译过程有关。
int add(int a, int b, int c);
int main()
{
add(10, 20); //错误
//错误详情:“add”: 函数不接受 2 个参数
return 0;
}
int add(int a, int b, int c = 30)
{
return a + b + c;
}
int add(int a, int b, int c = 30);
int main()
{
add(10, 20);
return 0;
}
int add(int a, int b, int c = 30)//错误
{ //错误详情: “add”: 重定义默认参数 : 参数 1
return a + b + c;
}
new
其功能同malloc相同,用于开辟空间,但是有一定的区别
格式:数据类型 *指针变量名 = new 数据类型;
delete
其功能与free相同,用于释放空间,但是有一定的区别
格式:delete 指针变量名
注意事项
delete不允许重复释放空间。此种写法6行已经释放了4行申请的内存,则7行在释放不属于他的内存,会造成运行阶段错误。
int main()
{
int * p = nullptr;
p = new int;
delete p;
delete p;
}
同一个指针变量不允许指向多个动态内存,此种写法虽然不会有语法错误,但会造成内存泄露。因为4行申请的内存空间并没有被释放。
int main()
{
int * p = nullptr;
p = new int;
p = new int;
delete p;
}
开辟和释放连续的空间
int main()
{
int * p = nullptr;
p = new int[10];
delete[] p;
}
inline修饰类的成员函数为内联函数
系统在进行函数调用的时候会进行分配内存、入栈、出栈等一系列活动,这样会增加内存开销影响运行时间。
而inline可以在一定程度上解决这个问题。
原理是,被inline修饰的函数,会在编译期间被插入到调用inline的位置,这样执行到该位置时就不会产生函数调用了。
用法:
隐式声明:
定义在类中的成员默认是内联的
显式声明:
先声明后实现,inline关键字写在声明处。
注意:
C语言中
static可以修饰变量或函数,会将变量或函数的连接属性由外连接转为内连接,即只能在内部调用,无法在外部调用。同时也会改变其生命周期。
static修饰局部变量,局部变量的生命周期就被延长了,不会随着函数的出栈而释放。
static修饰全局变量,全局变量就不能被别的文件使用,仅仅能在本文件内部使用
static修饰函数,函数只能在本文件内部使用。
C++中
在C++中static不仅能完全兼容C语言的特点,还可以修饰类的成员变量和成员函数。
修饰成员变量
当一个变量被static修饰之后,其就不属于某个对象了,而是被所有对象共享,成为了静态成员变量。
普通成员变量,在对象构造的时候才分配内存,但静态成员属性在对象产生之前就已经
在内存中产生了。
由于静态成员变量不属于某个对象,所以其有一个特别的使用方式:
类名::静态成员变量
静态成员变量必须被初始化
普通成员变量在对象构造的时候通过构造函数初始化,但是静态成员变量在对象创建之前就存在了,所以其不在构造函数中初始化。其在类外初始化。初始化方式:
数据类型 类名::静态变量名 = 值
class test
{
private:
int a;
int b;
public:
static int x;
test()
{
cout << "构造函数的调用" << endl;
}
~test()
{
cout << "析构函数的调用" << endl;
}
static void show()
{
cout << x << endl;
}
};
int test::x = 100;//注释掉该句,会出现错误,无法解析的外部命令
int main()
{
test::show();
test t1(3, 4);
cout << sizeof(t1) << endl;
test t2(5, 6);
cout << sizeof(t2) << endl;
cout << &t1.x << endl;
cout << &t2.x << endl;
}
//运行结果
//100
//构造函数的调用
//8
//构造函数的调用
//8
//001BB000
//001BB000
可以看到,两个对象调用的showAddr函数,显示两个对象显示的x地址相同,即是同一个x。
修饰成员函数
如果一个类的函数成员被static修饰,则该成员函数就被称为静态函数成员,不再属于某个对象。
静态成员函数因为其不属于任何对象,是脱离对象的存在,所以在对象创建之前就已经可以通过对象名进行调用。所以其可以直接调用类中的静态成员,但是还调用普通成员变量则需要借助对象。
class test{
private:
int a;
int b;
public:
static int x;
test(int a, int b):a(a), b(b) {
cout << "构造函数的调用" << endl;
}
~test() {
cout << "析构函数的调用" << endl;
}
static void showX(){
cout << x << endl;
}
static void showAll(test& pt){
cout << pt.a << endl;
cout << pt.b << endl;
cout << x << endl;
}
};
int test::x = 100;
int main(){
test::showX();
test t1(3, 4);
test::showAll(t1);
return 0;
}
//输出结果
//100
//构造函数的调用
//3
//4
//100
//析构函数的调用
注意
static不能修饰 构造函数、析构函数、拷贝构造函数
因为使用static修饰成员的目的是为了脱离对象,但是上面所说的桑函数却是为对象服务的,所以他们之间的目的是相悖的,所以无法使用static修饰。
this是一个隐藏在每一个非静态成员函数种的特殊指针,用于指向被成员函数操作的对象。
在一个对象调用类的成员函数时,这个兑现那个的地址会作为成员函数的参数传递进去,用来调用该对象的成员属性。这就是this指针。
this指针的传递是默认操作,我们不需要手动传递,也无法干涉,即使我们将成员函数的参数列表写成void也无法改变。
我们可以通过this指针来区分成员属性和同名形参。
C++中const和C语言中const的区别
C++中被const修饰的叫做常量,其使用任何方式都不能被修改
#include
int main()
{
const int a = 10;
int *p = &a; //错误
//错误详情:“初始化”: 无法从“const int *”转换为“int *”
int& pa1 = a; //错误
//错误详情:“初始化”: 无法从“const int *”转换为“int &”
const int& pa2 = a;//正确,但pa2已是const,无法修改
const int *pa3 = &a;//正确,但pa3已是const,无法修改
return 0;
}
C语言中被const修饰的叫做只读变量,其可以通过指针的方式修改
#include
int main()
{
const int a = 10;
int *p = &a;
*p = 20;
printf("%d\n", a);//输出结果为20
return 0;
}
常引用可以引用常量
这里的常量包括数字常量、字符常量、由自由变量编程的常量
常引用可以引用变量
此操作是将一个变量的功能常量化,所以可以。
常对象
被const修饰的对象叫做常对象。常对象,是一个常量,着很好理解,但是会发生一些有意思的现象。
class test{
private:
int a;
public:
test(int a ):a(a){
cout << "构造函数的调用" << endl;
}
void show() {
cout << a << endl;
}
~test()
{
cout << "析构函数的调用" << endl;
}
};
int main(){
const test t1(10);
t1.show(); //此处报错
//错误详情:“void test::show(void)”: 不能将“this”指针从“const test”转换为“test &”
return 0;
}
我们知道,常量不能够更改,但是我仅仅是通过show方法访问成员变量啊,并没有更改,为什么也会报错?
让我们仔细看一下错误提示,会发现这个错误和this指针扯上了关系。
我们知道,成员函数都有一个默认的参数,这是我们无法干涉的。在本例中这个this指针的类型应给应该是test*。但是对象t1确实一个常对象,一个常对象需要使用常量指针来指向,即const test*。这是与this指针不兼容的。
解决这个问题的办法也有,就是将函数变为常函数。
只需要在函数的末尾加上const即可。
class test{
private:
int a;
public:
test(int a ):a(a){
cout << "构造函数的调用" << endl;
}
void show() const{
cout << a << endl;
}
~test(){
cout << "析构函数的调用" << endl;
}
};
int main(){
const test t1(10);
t1.show();
return 0;
}
//输出结果
//构造函数的调用
//10
//析构函数的调用
常对象只能调用常函数。非常对象只能调用非常函数。
const可以作为函数重载的条件。
常数据成员
常数据成员就是被const修饰的数据成员。
其只能在创建时通过构造函数的初始化参数列表来进程初始化赋值。
赋值之后任何方式都无法修改。
final,最后的,不可更改的。
final修饰类
代表类不可被继承。
class Point final{
private:
int x, y;
public:
Point(int x, int y) :x(x), y(y) {};
};
class Demo : public Point //此处报错
{ //错误详情:"Demo"无法从"Point",因为它已被声明为final
Demo(int x, int y):Point(x, y) {};
};
final修饰成员函数
final修饰的类的成员函数必须是虚函数,派生类无法覆盖(重写)基类的该函数。
class Point{
private:
int x, y;
public:
Point(int x, int y) :x(x), y(y) {};
virtual void showData() final { cout << "final修饰虚函数" << endl; }
};
class Demo : public Point{
public:
Demo(int x, int y):Point(x, y) {};
void showData() { cout << "子类重写的系函数" << endl; }//此处报错
//"Point::shoeData":声明为"final"的函数无法被"Demo::showData"重写
};
int main(){
}
作用域
作用域:一个标识符在程序的正文中有效的区域
分类:函数原型作用域、局部变量作用域、类作用域、命名空间作用域
函数原型作用域:函数声明时,形式参数的作用域范围
int main(int a);//变量a的作用域在括号之间
局部变量作用域:从形式参数声明开始一直到函数结束
int fun()
{
int i = 10;
cout << i << endl;
}
int main()
{
i = 20; //错误
//错误详情:“i”: 未声明的标识符
return 0;
}
复合语句作用域:从变量声明处开始一直到复合语句结束,一般指变量所在的花括号结束
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
cout << "1111" << endl;
}
}
cout << j << endl; //错误
//错误详情:“j”: 未声明的标识符
return 0;
}
一个变量的实际作用域是受到多种约束的结果,如下函数中j变量。
int fun()
{
int i = 0;
for (i = 0; i < 5; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
cout << "1111" << endl;
}
}
cout << j << endl; //错误
//错误详情:“j”: 未声明的标识符
}
命名空间作用域
凡是在该空间内声明的,不属于前面所描述的各个作用域的标识符,都属于该命名空间作用域
命名空间:
一种代码组织的形式 通过名称空间来分类,区别不同的代码功能。
其作用是解决了命名危机。命名危机就是变量名或函数名的重名问题。
把不同的变量或函数放进不同的命名空间内,这样变量和函数就属于不同的命名空间了,这样只需要保证同一个命名空间内的变量、函数名不重复就行了。
创建:
命名空间可嵌套
namespace 命名空间名
{
}
使用:
命名空间名::标识符名称
其中::作用域区分符
void fun()
{
cout << "这是A命名空间外的fun函数" << endl;
}
namespace A
{
void fun()
{
cout << "这是A命名空间内的fun函数" << endl;
}
}
int main()
{
fun();
A::fun();
return 0;
}
//运行结果:
//这是A命名空间外的fun函数
//这是A命名空间内的fun函数
using namespace std; 表示使用std命名空间内的函数。
namespace A
{
void fun()
{
cout << "这是A命名空间内的fun函数" << endl;
}
}
int main()
{
using namespace A; //此句表示接下来所有调用到的fun函数都属于A命名空间
fun(); //此时调用fun函数即可不添加命名空间名::
return 0;
}
void fun()
{
cout << "这是A命名空间外的fun函数" << endl;
}
namespace A
{
void fun()
{
cout << "这是A命名空间内的fun函数" << endl;
}
}
int main()
{
using namespace A;
fun(); //错误,因为此时命名空间外有fun函数的同名函数
//错误详情:“fun”: 对重载函数的调用不明确
using A::fun;//表示接下来调用的fun函数都是A命名空间的
fun(); //正确
::fun(); //调用不属于任何一个命名空间的fun函数
return 0;
}
可见性
当程序运行到某一点时,此刻能够使用的标识符(变量和函数),就是在该处可见标识符
int t = 100
int main()
{
int t = 10;
{
t = 40;
}
cout << t << endl; //输出为10
return 0;
}
在执行输出语段时,全局变量t是不可见的,其只能看见作用域内离他最近的变量
对象,一些皆对象
类型,即对一类事物(对象)共有特点的概括
抽象
将一些事物的共有的店有侧重的抽象出来,即将对我们有用的特点抽象出来
数据特点抽象成 属性
行为特点抽象成 行为
封装
因为对同一类事物抽象出来的数据和行为本质四昂是描述的一类事物,是密不可分的。所以,应该将其结合起来看作一个整体。
封装就是,将属性和操作属性的函数结合起来,形成一个有机的整体
类
本质上是一个蓝图、说明,他告诉我们一个事物是什么样子,能够干什么。而并未创建任何个体。实际上就类似一个自定义的数据类型。
定义类
class 类名
{
访问权限:
成员属性和成员函数
访问权限:
成员属性和成员函数
}
类成员的访问权限:
一般来说类的成员属性都会被设为私有的,同时设立一些接口,使外部仅可以通过这些接口来访问或修改数据
成员函数有两种写法:
类内声明,类外实现。
类内声明,类内实现。
对象
对象是由类实例化出的个体。
如果类是一个图纸,如下图:
那么对象就是下图实物。
使用类实例化对象:
类名 对象名;
_使用成员函数和成员变量:
对象名.成员函数或者成员变量;
对象所占的存储空间:
class m
{
private:
int a;
int b;
int c;
public:
void setA(int a)
{
this->a = a;
}
void setB(int b)
{
this->b = b;
}
void setC(int c)
{
this->c = c;
}
};
int main()
{
m one;
cout << sizeof(one) << endl;
cout << sizeof(m) << endl;
return 0;
}
//输出结果均为12
上程序说明一个对象占空间的和成员属性有关,和成员函数无关。成员函数并不占存储空间。函数只有在运行过程中入栈才会占用空间。
其中sizeof(m)有结果并不说明类占存储空间,就如sizeof(int)一样,得出的结果并不是int占内存的大小,而是这个类型占内存的大小。
为了对对象安全的进行初始化,C++提供了初始化程序,叫做构造函数。通过构造函数可以将对象初始化为一个特定的状态,包括分配内存、初值等。
构造函数特点:
格式:
类名(参数列表)
{
实现体;
}
class test
{
private:
int a;
int b;
public:
test()
{
cout << "构造函数的调用" << endl;
}
test(int m_a, int m_b = 0)
{
a = m_a;
b = m_b;
cout << "两个参数的构造函数的调用" << endl;
}
void show()
{
cout << "a = " << a << "; b = " << b << endl;
}
};
int main()
{
test tmp1;
test tmp2(1, 2);
tmp2.show();
test tmp3(3);
tmp3.show();
return 0;
//输出显示:
//构造函数的调用
//两个参数的构造函数的调用
//a = 1 ; b = 2
//两个参数的构造函数的调用
//a = 3 ; b = 0
}
可以看到构造函数可以有参、可以重载、可以有默认参数。可以得出
构造与普通成员函数的区别:
在上函数中,虽然使用构造函数为成员属性赋初值,但是使用起来不方便,且当对象对类成员时会出现问题。这种问题可以使用初始化参数列表解决。
初始化参数列表:
是一种便捷的参数初始化方式。
格式:
构造函数名(参数列表) : 成员属性1(形参1), 成员属性2(形参2), 成员属性3(形参3)…
{
构造函数体;
}
注意:
调用无参构造函数的时候不能添加()
class test
{
private:
int a;
int b;
public:
test()
{
cout << "无参构造函数的调用" << endl;
}
test(int m_a, int m_b = 0) : a(m_a), b(m_b)
{
a = m_a;
b = m_b;
cout << "两个参数的构造函数的调用" << endl;
}
void show()
{
cout << "a = " << a << " ; b = " << b << endl;
}
};
int main()
{
test tmp1; //注意此处没有添加()
test tmp2(1, 2);
tmp2.show();
test tmp3(3);
tmp3.show();
return 0;
}
//输出结果
//无参构造函数的调用
//两个参数的构造函数的调用
//a = 1 ; b = 2
//两个参数的构造函数的调用
//a = 3 ; b = 0
我们知道在构造函数中可以为成员属性动态的分配内存,但是怎样生么时候去释放呢?答案就是析构函数。
C++编译器提供了析构函数,用来对对象的内存进行清理。
格式:
~类名()
{
函数体;
}
构造函数特点:
析构函数的使用:
class test
{
private:
int a;
int *arr;
public:
test(int m_a) : a(m_a)
{
cout << "无参构造函数的调用" << endl;
arr = new int[5];
int i = 0;
for (i = 0; i < 5; i++)
{
arr[i] = i;
}
}
~test()
{
cout << a << "的析构函数的调用" << endl;
delete[] arr;
}
void show(void)
{
int i = 0;
for (i = 0; i < 5; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
};
int main()
{
test tmp2(1);
tmp2.show();
tmp2.~test();
return 0;
}
//输出结果:
//无参构造函数的调用
//0 1 2 3 4
//1的析构函数的调用
手动调用析构函数测试:
class test
{
private:
int a;
int *arr;
public:
test(int m_a) : a(m_a)
{
cout << "无参构造函数的调用" << endl;
arr = new int[5];
int i = 0;
for (i = 0; i < 5; i++)
{
arr[i] = i;
}
}
~test()
{
cout << a << "的析构函数的调用" << endl;
delete[] arr;
}
void show(void)
{
int i = 0;
for (i = 0; i < 5; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
};
int main()
{
test tmp2(1);
tmp2.show();
tmp2.~test();
return 0;
}
//运行发生错误
//输出结果
//无参构造函数的调用
//0 1 2 3 4
//1的析构函数的调用
//1的析构函数的调用
上述结果可以看到,构造函数被创建一次,而析构函数却被调用两次。发生错误的原因就是因为,析构函数调用两次,那么动态分配的内存就会被释放两次,这是错误的。所以,尽量不要手动调用析构函数。
注意:
析构函数调用的顺序问题:同一作用域内,先构造的函数后析构。
见:
拷贝构造函数
见
无名对象
对象是否可以定义成全局?
答案是可以的。
class test
{
public:
test(int n):num(n)
{
cout << "构造函数的调用" << endl;
}
void show()
{
cout << "num的值为:" << num << endl;
}
~test()
{
cout << "析构函数的调用" << endl;
}
private:
int num;
};
test tmp(5);
int main()
{
cout << "*********" << endl;
tmp.show();
}
//输出结果:
//构造函数的调用
//*********
//num的值为:5
//析构函数的调用
有上述程序可知,对象是可以定义成全局对象的,且可以正常使用。
但是,有结果可以看到,main函数种我们首先打印了一行分隔符,但是问题来了。
分隔符是main函数种的语句,全局对象tmp的构造函数在main函数之前被调用,这和我们平时所认知的,main函数是程序的入口,是最开始执行的,所有函数都要靠main函数调用的常识有悖。但却是正常执行,那么,是调用了tmp的构造函数呢???
原因如下:
在大多数的实现方式里,核心会运行专门的启动代码,启动代码会在启动main()之前完成所有的初始化工作,这其中当然包括了全局对象的初始化。这个所谓的启动代码就是Runtime函数库的Startup代码。
在程序执行时,系统会先调用Startup,完成函数库初始化、进程信息设立、I/O stream产生,以及对static对象的初始化等动作。然后Startup调用main()函数,把控制权交给main()函数。main()函数执行完毕,控制权交回给Startup,进行反初始化动作。
一个类中内嵌了其他类的对象作为成员属性的情况,类和内嵌对象成员之间的关系是包含和被包含的关系。
当创建一个类的对象时,如果该类内部有内嵌的成员对象,则优先创建内嵌对象。
析构一个对象的时候,先析构外部对象,再析构内嵌的对象。
友元是定义在外部的函数或者类,它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。友元不是类的成员函数,但是它可以访问类中的私有成员
class test{
private:
int a;
public:
friend void showA(test& t);
test(int a ):a(a) {
cout << "构造函数的调用" << endl;
}
~test() {
cout << "析构函数的调用" << endl;
}
void show() {
cout << a << endl;
}
};
void showA(test& t) {
cout << t.a << endl;
t.a = 1000;
}
int main(){
test t1(10);
//cout << t1.a << endl;//此处报错,故注释掉
//错误详情:“test::a”: 无法访问 private 成员(在“test”类中声明)
showA(t1);
t1.show();
return 0;
}
//输出结果
//构造函数的调用
//10
//1000
//析构函数的调用
可以看到,在主函数中直接输出t1对象中的成员变量a是不行的,因为a是t1的私有成员。
但是在showA函数种却将a正确输出了,同时还对a进行了更改。这就是友元。
通过==friend void showA(test& t);==语句,声明了showA函数是test类的友元函数,使其可以访问更改其中私有成员。
class test{
friend class demo;
private:
int a;
public:
test(int a ):a(a) {}
void show() {
cout << a << endl;
}
};
class demo {
public:
void showTest(test& t) {
cout << t.a << endl;
}
};
int main(){
test t1(10);
demo d;
t1.show();
d.showTest(t1);
return 0;
}
//输出结果
//10
//10
上程序中可以看到我们在一个类中输出了另一个类的私有成员。
这是因为通过==friend class demo;==语句,声明了demo函数是test类的友元类,使其可以访问更改其中私有成员。
注意
友元的出现是为了是实现数据共享,但是却破坏了封装性
友元具有单向,不可传递,不可继承的特性
指向数据成员
声明:
类型说明符 类名::*指针名 = &类名::数据成员名;
此种方式属于在类外调用数据成员,所以只能指向公共数据成员。
class test{
public:
int a;
int b;
test(int a, int b):a(a), b(b) {
cout << "构造函数的调用" << endl;
}
~test()
{
cout << "析构函数的调用" << endl;
}
};
int main(){
int test::*p1 = &test::a;
int test::*p2 = &test::b;
test t1(10, 20);
cout << p1 << endl;
cout << p2 << endl;
//cout << *p1 << endl; //此句报错
//错误详情:“*”:“int test::* ”类型的操作数非法
cout << t1.*p1 << endl;
cout << t1.*p2 << endl;
return 0;
}
//输出结果
//构造函数的调用
//1
//1
//10
//20
//析构函数的调用
int main()
{
int a = 10;
int *p = &a;
cout << p << endl;
}
//输出结果
//006FF7B4
可以看到,p1、p1虽然叫做指针,但是与传统意义上的指针并不相同。若将传统意义上的指针输出,输出结果为地址标号。而p1、p2输出结果却为1。
其只有在对象创建之后,通过类名来访问。
&test::a代表着a相对于类的偏移
指向函数成员
声明:
类型说明符 (类名::*指针名)(参数列表) = &类名::函数名
class test{
private:
int a;
int b;
public:
test(int a, int b):a(a), b(b) {
cout << "构造函数的调用" << endl;
}
~test()
{
cout << "析构函数的调用" << endl;
}
void show() {
cout << a << ", " << b << endl;
}
};
int main(){
void (test::*pShow)() = &test::show;
cout << pShow << endl;
test t1(10, 20);
(t1.*pShow)();
}
//输出结果:
//1
//构造函数的调用
//10, 20
//析构函数的调用
函数模板 将函数的数据类型进行忽略,仅仅关注函数的实现逻辑,这种技术叫做泛型,能够适应各种类型的变量使用该逻辑来实现业务。
格式:
template <typename T> //此句的作用是告诉编译器。开始泛型,遇到T不要报错,其代表了一种数据类型。
返回值类型 函数名(参数列表)
{
函数体;
}
其中template句的作用是告诉编译器。开始泛型,遇到T不要报错,其代表了一种数据类型
template <typename T1, typename T2>
void showAdd(T1 a, T2 b){
cout << a << " + " << b << " = " << a + b << endl;
}
int main(){
int a = 10;
double b = 20.6;
showAdd(a, b); //隐式调用
showAdd<int, double>(a, b); //显式调用
return 0;
}
//输出结果:
//10 + 20.6 = 30.6
//10 + 20.6 = 30.6
注意:显示调用的时候实际的数据类型应该同输入的一直,否则会将实际的数据转换为输入的类型。
基本概念
在原有类的基础上进行更为具体更详细的定义
从原有的类拿来东西并创建新类的过程叫做继承。
原有类产生新类的过程叫做派生。
原有的类称为基类或者父类,新的类叫做子类或者派生类。
单继承:一个子类只能有一个父类。
多继承:一个子类可以有多个父类。
语法格式:
class 派生类名:继承方式基类名1, 继承方式 基类名2, …
{
}
class Base{
private:
int a;
public:
Base() { cout << "Base的构造函数" << endl; }
~Base() { cout << "Base的析构函数" << endl; }
};
class Derived : public Base{};
int main(){
Derived d;
return 0;
}
//运行结果
//Base的构造函数
//Base的析构函数
可以看到,子类调用了父类的构造函数和析构函数
这是因为子类从父类继承了所有东西,包括父类的数据空间,虽然子类有自己的数据空间,但是子类并不知道怎样初始化父类的空间,所以需要调用父类的构造函数来初始化父类的空间。
同时,子类也不知道怎样清理从父类继承过来的数据空间,所以需要调用从父类继承而来的数据空间。
class Base{
private:
int a;
public:
Base(int a):a(a) { cout << "Base的构造函数" << endl; }
~Base() { cout << "Base的析构函数" << endl; }
};
class Derived : public Base{};
int main(){
Derived d;//此处报错
//错误详情:“Derived::Derived(void)”: 尝试引用已删除的函数
return 0;
}
上面函数错误的原因
父类的构造函数是一个带有参数的构造函数,所以由父类派生出来的子类的默认构造函数就会被删除。
正确写法:
class Base{
private:
int a;
public:
Base(int a):a(a) { cout << "Base的构造函数" << endl; }
~Base() { cout << "Base的析构函数" << endl; }
};
class Derived : public Base{
public:
Derived(int a):Base(a) { cout << "Derived的构造函数" << endl; }
~Derived() { cout << "Derived的析构函数" << endl; }
};
int main(){
Derived d(1);
return 0;
}
//输出结果:
//Base的构造函数
///Derived的构造函数
//Derived的析构函数
//Base的析构函数
同时,由上函数的结果我们可以看到,构造析构函数的调用顺序,
先调用父类的构造函数然后调用子类的构造函数,先调用子类的析构函数,然后再调用父类的析构函数
如果是多继承,则调用顺序同继承顺序一致。
继承方式共有public、private、protected三种。
在父类中,父类的成员也有三种权限,如下所示:
继承方式的不同会导致子类在访问父类成员时有区别,如下表:
指在需要使用基类对象的任何地方,都可以使用该基类的公共派生类来代替。
通过公有继承,派生类得到了基类中除了构造、析构、静态成员以外的所有成员。这样,共有派生类实际上就具备了激烈所具备的所有功能,方式能用基类解决的问题,都能用派生类解决。
类型兼容性使用常见包括如下情况:
静态成员在继承中不会被派生类继承,而是父子间共享
因为父类的友元和子类没有关系,所以友元不能被继承
多态的定义简单来说就是使一条语句有多种状态。
方式一:
方式二:
将对象和对象的操作方法结合的过程称为绑定,也可以将上面两种多态称为静态绑定和动态绑定。
动态绑定也成为“晚绑定”或“迟绑定”
重载多态、参数多态、强制多态都属于编译时多态。
包含多态属于运行时多态。
虚函数:是在类中的非静态成员函数声明前加上vritual关键字的成员函数。
成为虚函数后,能够实现多态,让基类指针接收不同的对象,并调用同名函数实现不同操作。
未使用虚函数:
class Animal{
public:
void speak(){
cout << "我是一只小动物" << endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout << "我是一只猫,快乐的星猫" << endl;
}
};
void AnimalSpeak(Animal& a){
a.speak();
}
int main(){
Animal a;
Cat c;
AnimalSpeak(a);
AnimalSpeak(c);
}
//运行结果
//我是一只小动物
//我是一只小动物
可以看到,虽然AnimalSpeak函数所需参数是Animal类型的引用,但是我们依然可以使用Cat类的对象传入函数内,因为Cat是Animal类的子类。
但是在只依然调用的是Cat从Animal那里继承来的speak方法,无法调用子类所重写的方法。
使用virtual之后:
class Animal{
public:
virtual void speak(){
cout << "我是一只小动物" << endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout << "我是一只猫,快乐的星猫" << endl;
}
};
void AnimalSpeak(Animal& a){
a.speak();
}
int main(){
Animal a;
Cat c;
AnimalSpeak(a);
AnimalSpeak(c);
}
//运行结果
//我是一只小动物
//我是一只猫,快乐的星猫
可以看到,当使用了virtual之后,在AnimaSpeak函数中调用了Cat对象的方法,并没有调用父类Animal的方法。
class Animal{
public:
virtual void speak(){
cout << "我是一只小动物" << endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout << "我是一只猫,快乐的星猫" << endl;
}
};
class SmallCat :public Cat{
public:
void speak() {
cout << "我还小呢" << endl;
}
};
void AnimalSpeak(Animal& a){
a.speak();
}
int main(){
Animal a;
Cat c;
SmallCat sc;
AnimalSpeak(a);
AnimalSpeak(c);
AnimalSpeak(sc);
}
//运行结果
//我是一只小动物
//我是一只猫,快乐的星猫
//我还小呢
可以看到,当父类的一个函数声明为虚函数之后,父类的子类,子类的子类的该函数都是虚函数。所以子类的函数前面的virtual可加可不加。
虚函数产生了一种很神奇的现象。产生这种现象的原因就是虚函数表。
class Animal{
int age;
public:
void speak(){
cout << "我是一只小动物" << endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout << "我是一只猫,快乐的星猫" << endl;
}
};
int main(){
cout << "Animal类的大小:" << sizeof(Animal) << endl;
cout << "Cat类的大小:" << sizeof(Cat) << endl;
}
//输出结果
//Animal类的大小:4
//Cat类的大小:4
在类中,只有属性才占用内内存,所以,Animal类的大小是4,而Cat类继承了Animal类中的age属性,所以大小也是4。
加上virtual之后,运行结果如下:
Animal类的大小:8
Cat类的大小:8
内存的占用情况变了,子类和父类都多了4个字节。那么这四个字节是什么呢?
我们可以使用一种非常流氓的方式来看一下类中的内存分布。
class Animal{
int age;
public:
Animal(int age):age(age) {}
void speak(){
cout << "我是一只小动物" << endl;
}
};
class Cat:public Animal{
public:
Cat(int age) :Animal(age) {}
void speak(){
cout << "我是一只猫,快乐的星猫" << endl;
}
};
int main(){
Animal a(17);
int* pa = (int*)&a;
cout << "*pa = " << *pa << endl;
cout << "a的大小:" << sizeof(a) << endl;
Cat c(24);
int* pc = (int*)&c;
cout << "*pc = " << *pc << endl;
cout << "c的大小:" << sizeof(c) << endl;
}
//运行结果
//*pa = 17
//a的大小:4
//*pc = 24
//c的大小:4
可以看到,a占4个字节空间,我们通过地址将这个空间的值输出,结果就是属性age的值,c对象的情况也是一样。
加上virtual之后:
int main(){
Animal a(17);
int* pa = (int*)&a;
cout << "a的大小:" << sizeof(a) << endl;
cout << "*pa = " << *pa << endl;
cout << "*(pa+1) = " << *(pa + 1) << endl;
Cat c(24);
int* pc = (int*)&c;
cout << "c的大小:" << sizeof(c) << endl;
cout << "*pc = " << *pc << endl;
cout << "*(pc+1) = " << *(pc + 1) << endl;
}
//运行结果
//a的大小:8
//*pa = 13802532
//*(pa+1) = 12
//c的大小:8
//*pc = 13802300
//*(pc+1) = 23
可以看到原本多出来的一个字节变成了一串数字,而原本的age被放到了后面。
这多来的四个字节放置的并不是数字,而是一个指针,这个指针的名字叫做虚函数表指针。
该指针指向了一个叫做虚函数表的表。表中存放的是一个指针,一个函数指针。
当我们没有使用虚函数的时候,a对象占用4个字节,c对象同样占用4个字节,其中都存储着age属性。
但是当我们使用了虚继承之后,a对象的大小就变为8个字节,前四个字节地址存储着虚函数指针,它指向了父类的虚函数表,而虚函数表中存储着父类虚函数的地址。当我们调用父类对象的speak方法时,就会执行虚函数表中指向的函数。
当子类继承了父类,子类同样也会创建一个虚函数表,若子类不重写父类的虚函数,则子类中的虚函数表会存储着父类虚函数的地址,但是若子类重写了父类的虚函数,子类虚函数表中的函数指针就编程指向子类自己重写的虚函数的指针。
这样我们就可以调用不同的方法了。
虚析构函数,用于在使用基类指针的时候,调用释放的析构函数对不同的对象实现清理。
class A{
char* pstr;
public:
A() {
cout << "父类构造方法" << endl;
pstr = new char;
}
~A() {
delete pstr;
cout << "父类析构函数" << endl;
}
};
class B :public A{
int *pint;
public:
B() {
pint = new int;
cout << "子类构造函数" << endl;
}
~B() {
delete pint;
cout << "子类析构函数" << endl;
}
};
int main(){
A *pa = new B;
delete pa;
return 0;
}
//运行结果
//父类构造方法
//子类构造函数
//父类析构函数
我们知道,new一个新对象时,会调用构造方法,既然是子类对象,所以自然会调用父类和子类的构造方法。然后使用一个父类指针指向这个子类对象。
但是释放对象的时候却出了问题,子类对象中有动态内存,到那时释放父类指针指向的子类对象的时候却没有调用子类的析构函数,这样,子类对象中的内存并未释放完全,会造成内存泄露。
class A{
char* pstr;
public:
A() {
cout << "父类构造方法" << endl;
pstr = new char;
}
virtual ~A() {
delete pstr;
cout << "父类析构函数" << endl;
}
};
//运行结果
//父类构造方法
//子类构造函数
//子类析构函数
//父类析构函数
可以到通拓父类指针释放子类对象的时候,子类的析构函数也被调用了。所以子类对象的内存被完全释放。
重载:
相同作用域内,函数名相同,参数类型或个数或顺序不同,产生函数重载
隐藏:
派生类中有一个和基类同名函数,则派生类的函数隐藏了基类中所有的同名函数
重定义:
有派生关系的父子类,子类的某个成员函数和父类的成员函数同名,同参,没有virtual关键字。称为子类重定义了父类函数
覆盖:(虚函数重写)
有派生关系的父子类,子类的某个成员函数和父类的成员函数同名、同参,且父类的函数有virtual关键字。称为子了虚函数覆盖了父类虚函数。
class A{
public:
void fun1111() { cout << "这是A类中的fun1111,无参版本" << endl; }
void fun1111(int x) { cout << "这是A类中的fun1111,int类型参数" << endl; }
void fun1111(int x, int y) { cout << "这是A类中的fun1111,两个参数" << endl; }
virtual void fun(char c) { cout << "这是A类中的fun1111,虚函数版本" << endl; }
void fun2222() { cout << "这是A类中的fun2222" << endl; }
};
class B :public A{
public:
void fun1111(double x) { cout << "这是B类中的fun1111,double参数" << endl; }
};
int main(){
B b;
//b.fun1111(10); //程序报错
//b.fun1111('c'); //程序报错
//b.fun1111(3, 4); //程序报错
//错误详情:不接受n个参数
b.fun1111(3.14);
b.A::fun1111(3, 4);
b.fun2222();
return 0;
return 0;
}
//运行结果
//这是B类中的fun1111,int类型参数
//这是B类中的fun1111,int类型参数
//这是A类中的fun1111,两个参数
//这是A类中的fun2222
在同一作用域下,即父类内部有多个同名参数,他们同名但不同参,这就时重载
可以看到,当子类中有一个父类同名函数(不管参数是否相同),子类都不能直接调用父类的函数,若想调用父类的函数,需要加作用域。这就是隐藏
class A{
public:
void fun1111() { cout << "这是A类中的fun1111,无参版本" << endl; }
void fun1111(int x) { cout << "这是A类中的fun1111,int类型参数" << endl; }
void fun1111(int x, int y) { cout << "这是A类中的fun1111,两个参数" << endl; }
virtual void fun(char c) { cout << "这是A类中的fun1111,虚函数版本" << endl; }
void fun2222() { cout << "这是A类中的fun2222" << endl; }
};
class B :public A{
public:
void fun1111(int x) { cout << "这是B类中的fun1111,int类型参数" << endl; }
void fun1111(char c) { cout << "这是B类中的fun1111,虚函数版本" << endl; }
};
int main(){
B b;
b.fun1111(10);
b.fun1111('c');
b.fun2222();
return 0;
}
//运行结果
//这是B类中的fun1111,int类型参数
//这是B类中的fun1111,虚函数版本
//这是A类中的fun2222
可以看到,子类中分别有两个与父类中同名同参的函数。
其中void fun1111(int x)由于在父类中没有virtual修饰,所以其就是重定义。
而void fun1111(char c)由于在父类中有virtual修饰,所以其就是虚函数重写或覆盖。
注意:
不管是隐藏、重定义、重写还是重载,都是同名函数函数之间的关系。和不同名函数没有关系。
抽象类是一种特殊的类,是为了抽象和设计的目的而创造的。,通过他的多态使用。抽象类属于类层次的上层,一个抽象类无法实例化对象,仅仅是一个虚构的概念。只能通过继承机制来生成抽象类的非抽象派生类,然后实例化。
纯虚函数
在基类中没有定义内容,仅有一个声明,需要在派生类中定义出具体的内容
格式:
virtual 函数返回值类型 函数名(参数列表) = 0;
抽象类
作为一个公共的接口,而接口的完整实现交给派生类。
抽象类的派生类若不实现抽象类中的纯虚函数,则它依旧是抽象类。
class Person{
private:
int age;
public:
virtual void showData() = 0;
};
class child:public Person{
public:
void showData() { cout << "这是小孩" << endl; }
};
class oldman :public Person{};
int main(){
child c;
c.showData();
//oldman o;//此处报错
//错误详情:"oldman"无法实例化抽象类
}
//运行结果:
//这是小孩
C语言风格的类型转换属于强制类型转换。
TYPE a = (TYPE)b;
C++风格的类型转换分为四种:
static_cast 静态类型转换
reinterpret_cast 重新解释类型
dynamic_cast 动态类型转换
const_cast 去掉const属性
static_cast
C语言中可以是想隐式类型转换的,在C++中都能实现静态类型转换,编译器在编译时会进行类型检查,除了指针类型的转换。
double f1 = 3.14
C语言中将f1赋值给int类型:
int m = f1; //隐式类型转换
C++中将f1赋值给int类型:
int m = static_cast<int>(f1);
其主要有以下几种用法:
reinterpret_cast
重新解释类型:C语言中事项的强制类型转换在C++中可以使用从新解释类型转换方式进行转换。
在C++中,reinterpret_cast主要有以下几种强制转换用途:
格式:reinterpret_cast
注:type-id必须是一个指针、引用、算术类型、函数指针或成员指针。
const_cast
用来退去常量性。
C++中被const修饰的称为常量,是不可以被更改的。而const_cast正是用来退去这种常量性的。
但是这种使用有时候是很危险的。
int main(){
const int a = 10;
int* p = const_cast<int*>(&a);
*p = 30;
cout << "*p = " << *p << endl;
cout << "a = " << a << endl;
}
//运行结果
//*p = 30
//a = 10
在上面的程序中,a是一个被const修饰的量,所以,a是一个常量,是不可以修改的,然后我们通过const_cast褪去了a的常量性,使用一个int*类型的指针指向了a,并对a进行了更改。
但是结果却并不像我们想像的那样, *p的值变了,但是a的值没变。这是一个令人难以理解的现象, *p应该就是a啊,为什么会出现两个值?
我们通过调试来看一下:
可以看到,p指向的空间是0x005bf724,a的地址也是0x005bf724,没有问题,最后,在程序执行完毕时,a的值确实是变成了30,从地址中能够看得出来,到那时结果却依旧没变。
事实上,我们的这种修改常量值的语句可以称为未定义语句,就是说在标准C+规范中并没有明确规定这种与语句的具体行为,该语句的具体行为由编译器习性决定如何处理。
我们应该对这种未定义语句予以避免。所以,我们使用const_cast时一定要保证,退去const属性后,真正的原始空间时可以修改的。
dynamic_cast
动态类型转换:可以将父类当作子类来使用
其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL
使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。