函数分文件编写

1
2
3
4
创建.h后缀名头文件
创建.cpp后缀名源文件
在头文件中写函数的声明
在源文件中写函数的定义
指针在32位机器下(×86)通通占4个字节,不管是什么数据类型
指针在64位机器下通通占8个字节,不管是什么数据类型
1
2
3
4
5
6
7
8
9
10
int main()
{
//指针所在内存空间
int a=10;
int *p=&a;
cout<<"sizeof(int *)="sizeof(int *)<<endl; //4
cout<<"sizeof(float *)="sizeof(float *)<<endl; //4
cout<<"sizeof(double *)="sizeof(double *)<<endl; //4
cout<<"sizeof(char *)="sizeof(char *)<<endl; //4
}

空指针和野指针

1
2
3
4
5
6
7
8
//空指针:指针变量指向内存中编号为0的空间
//用途:初始化指针变量
//注意:空指针指向的内存是不可以访问的(因为0~ 255的内存是系统占用的,如果访问会报错)
int *p=NULL;
//如果*p=100;这样就会冲突

//野指针(获取访问权限冲突)
//*p=(int *)0×1100;

总结:空指针和野指针都不是我们申请访问的空间,因此不要访问

const修饰指针

1.const修饰指针-----常量指针 指针指向可以改,指针的指向值不可以改
2.const修饰常量-----指针常量 指针指向不可以改,指针的指向值可以改 引用的本质在C++内部实现是一个指针常量(引用一旦初始化就不能发生改变,变成别的变量的别名)所有的指针操作都是编译器做的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void func(int& ref)
{
ref=100;//ref是引用,转换为*ref=100
}
main()
{
int a=10;

//自动转换为 int* const ref=&a;指针常量是指针指向不可以改,也说明为什么引用不可以改
int& ref=a;
ref=20;//内部发现ref是引用,自动帮我们转换成:*ref=20;

cout<<"a:"<<a<<endl;
cout<<"ref:"<<ref<<endl;

fun(a);
return 0;
}
3.const既可以修饰指针也可以修饰常量 指针指向和指针的指向都不可以改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//常量指针
const int *p=&a;
*p=20; //错误
p=&b; //正确

//指针常量
int * const p=&a;
*p=20; //正确
p=&b; //错误

//const既修饰指针又修饰常量
const int * const p=&a;
*p=20; //错误
p=&b; //错误

结构体中const的使用场景(const用来防止误操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct student()
{
//姓名
string name;
//年龄
int age;
//分数
int score;
};

//将函数中的形参改为指针,可以减少内存空间,而且不会复制新的副本出来,但是只要在自定义函数中不小心误操作导致s变量的值更改的话,外部结构体变量信息也会更改,所以可以在形参前加一个const,这时候如果再更改结构体的数据时就会提示错误
void printStudent(const student *s)
{
//s->age=150;
cout<<"姓名"<<s.name<<"年龄"<<s.age<<s.age<<"得分:"<<s.score<<endl;
}

/*
void printStudent(student s)
{
cout<<"姓名"<<s.name<<"年龄"<<s.age<<s.age<<"得分:"<<s.score<<endl;
}
*/

int main()
{
//创建结构体变量
struct student s={"张三",15,70};
//通过函数打印结构体变量的信息
// printStudents(s);
printStudents(&s);
cout<<"main中张三的年龄为"<<s.age<<endl;
system("pause");
}

常量引用:使用场景是 用来修饰形参,防止误操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void show(const int &val)
{
//val=1000;//在形参上加上const就可以防止误操作,这时修改引用的别名就会报错:表达式必须是可以修改的左值
cout<<"val="<<val<<endl;
}
int main()
{
// int &ref=10;//引用必须引一块合法的内存空间,这样的写法是错误的,要么引用栈上的内存空间要么引用堆上的内存空间,而10存放在常量池中

//加上const之后 编译器将代码修改 int temp=10;const int & ref=temp;
const int &ref=10;

int a=100;//打印a的值
showValue(a);
cout<<"a:"<<a<<endl;

system("pause");

return 0;
}

内存分区模型

  • 代码区:存放函数体的二进制代码,由操作系统进行管理(共享和只读)

  • 全局区:存放全局变量和静态变量以及常量(字符串常量和其他常量,const修饰的变量)(这个区域的数据在程序结束后由操作系统释放)

    • const修饰的全局变量 int c_g_a=10 //c-const g-global

    • const修饰的局部变量int c_l_b=10 //c-const l-local

      • 注意:const修饰局部变量是和局部变量放在一起;而const修饰的全局变量,我们称之为全局常量,在全局区内存放
  • 栈区:由编译器自动分配释放,存放函数的函数值,局部变量等

  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统自动回收

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int * func()
    {
    //利用new关键字 可以将数据开辟到堆区
    //指针 本质也是局部变量,放在栈上(堆区的地址编号用栈上的指针保存),指针保存的数据是放在堆区
    int *p=new int(10);//本质是把堆区存放的地址返回给你,利用new创建的数据会返回该数据所对应的类型的指针
    return p;
    }
    int main()
    {
    //在堆区开辟数据
    int *p=func();
    cout<<*p<<endl;
    system("pause");
    }

    new操作符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    //1.new的基本语法
    int * func()
    {
    //在堆区创建整形数据
    //new返回的是 该数据类型的指针
    int *p=new int(10);
    return p;
    }
    void text01()
    {
    int *p=func();
    cout<<*p<<endl;
    //堆区的数据由程序员管理开辟,由程序员管理释放
    //如果想释放堆区的数据,利用关键字delete
    }
    //2,在堆区利用new开辟一个数组
    void text02()
    {
    //创建10整形数据的数据,在堆区
    int * arr=new int[10];//返回一个首元素地址
    for(int i=0;i<10;i++)
    {
    arr[i]=i+100;//给10个元素赋值 100~109
    }
    for(int i=0;i<10;i++)
    {
    cout<<arr[i]<<endl;
    }
    //释放堆区数组 要加[]才可以
    delete[]arr;

    }
    int main()
    {

    }

注意:代码区和全局区都是程序在运行前的一个区域(C++中在程序运行前分为全局区和代码区),在程序运行后才会有栈区和堆区

引用(给变量起别名)

数据类型 &别名=原名

引用的注意事项:

1.引用必须要初始化

2.引用一旦初始化就不可以更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14


int main()
{
int a=10;
//int &b; //错误的
int &b=a;

int c=20;
b=c;//赋值操作而不是更改引用
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
cout<<"c="<<c<<endl;
}

引用做参数函数

作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//值传递
void mySwap01(int a,int b)
{
int temp=a;
a=b;
b=temp; //值传递中形参确实发生改变,但实参没有发生改变
}

//址传递
void mySwap02(int *a,int *b)
{
int temp=*a;
*a=*b;
*b=temp; //址传递中形参和实参都发生改变
}

//引用传递
void mySwap03(int &a,int &b)
{
int temp=a;
a=b;
b=temp; //引用传递中形参和实参都发生改变
}
int main()
{
int a=10;
int b=20;
// mySwap01(a,b); //值传递
// mySwap02(&a,&b); //址传递
// mySwap03(a,b); //引用传递
cout<<"a="<<a<<endl; // 值传递:10 地址传递: 20 引用传递: 20
cout<<"b="<<b<<endl; //值传递:20 地址传递: 10 引用传递: 10
}

引用做函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//不要返回局部变量的引用
//函数的调用可以作为左值

int& text01()
{
int a=10;//局部变量存放在四区中的栈区
return a;
}

int& text02()
{
static int a=10;//静态变量存储在全局区,全局区上的数据在程序结束后系统释放,而不是在text02运行完就失效
return a;
}

int main()
{
// int &ref=text01();
// cout<<"ref="<<ref<<endl;//ref=10 第一次结果正确,因为编译器做了保留
// cout<<"ref="<<ref<<endl;//ref=260398 第二次结果错误,因为a的内存已经释放
//所以不要返回局部变量的引用

int &ref=text02();
cout<<"ref="<<ref<<endl; //ref=10
cout<<"ref="<<ref<<endl; //ref=10
cout<<"ref="<<ref<<endl; //ref=10

text02()=1000;//如果函数的返回值是引用,这个函数调用可以作为左值
cout<<"ref="<<ref<<endl; //ref=1000
cout<<"ref="<<ref<<endl; //ref=1000
}

函数提高

函数默认参数

语法:返回值类型 函数名(形参=默认值)
1
2
3
4
5
6
7
8
9
10
11
12
int func(int a,int b=20,int c=30)//写了一个b和c的默认值,当没有给参数默认值时,形参和实参的数量要一样,当有默认值就不需要相同数量的形参和实参了
{
return a+b+c;
}
int main()
{
cout<<func(10)<<endl; //60
cout<<func(10,30)<<endl; //70 如果这个值我传了就用我传的值,如果我没传就用默认值
system("pause");

return 0;
}
注意事项:1.如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值 例如: int func2(int a,int b=10,int c=20,int d=30)
2.如果函数的声明有默认参数,那么函数实现就不能有默认参数(声明和实现只能有一个有默认参数)
1
2
3
4
5
int func3(int a=10,int b=10);//函数的声明
// int func3(int a=10,int b=10) //函数的实现,因为此时函数的声明有默认参数,那么函数实现就不能有默认参数,不然就会报错:重定义了默认参数, 即使声明和实现传的默认参数一样 这个情况在C++中称之为"二异性"
// {
// return a+b;
// }

函数占位参数

语法:返回值类型 函数名 (数据类型){} //小括号中只有一个数据类型,没有参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
1
2
3
4
5
6
7
8
9
10
void func(int a,int)//占位参数还可以有默认参数  void func(int a,int =10)
{
cout<<"this is func"<<endl;
}
int main()
{
func(1010); //第二个10是没有办法传到func函数中

system("pause");
}
目前阶段占位函数用不到

函数重载(函数名可以相同)(析构函数没有参数,因此它不能被重载。一个类可以有多个构造函数,但是只能有一个析构函数)

作用:函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下(全局作用域)
  • 函数名称相同
  • 函数参数类型不同 或者 个数不同 或者 顺序不同
注意:函数的返回值不可以作为函数重载的条件(无法重载仅仅按返回类型区分的函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//1
void func()
{
cout<<"func的调用"<<endl;
}
//2
void func(int a) //1和2函数类型的个数不同
{
cout<<"func(int a)的调用"<<endl;
}
//3
void func(double a) //2和3函数函数参数类型不同
{
cout<<"func(double a)的调用"<<endl;
}
//4
void func(int a,double b) //4和5函数参数顺序不同
{
cout<<"func(int a,double b)的调用"<<endl;
}
//5
void func(double a,int b)
{
cout<<"(double a,int b)的调用"<<endl;
}
//6
// void func(double a,int b) //无法重载仅仅按返回类型区分的函数)
// {
// cout<<"(double a,int b)的调用"<<endl;
// }

int main()
{
//func(); //func(int a)的调用
//func(10);
//func(2.12);
func(2.12,10);
system("pause");
}

函数重载注意事项

1.引用作为重载条件

2.函数重载遇到默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//1.引用作为重载条件
void func(int &a) //1
{
cout<<"func(int &a)调用"<<endl;
}
void func(const int &a) //2 //1和2的形参类型不一样,可以发生函数重载
{
cout<<"func(const int &a)调用"<<endl;
}

int main()
{
int a=10;
func(a); //代码调用函数结果是调用1,没有加const 原因是变量a(变量的意思是可读,可写,可修饰);而加了const就是为了限制a的只读状态,只能读,不能写,所以调用1

func(10); //这时候就会调用2 const int &a=10; 计算机的优化功能
system("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//2.函数重载遇到默认参数
void func2(int a,int b) //1
{
cout<<"func2(int a,int b)"<<endl;
}
void func2(int a) //2
{
cout<<"func2(int a)"<<endl;
}


int main()
{
// func2(10); //这时候就会出错因为1和2都会被调用 当函数重载碰到默认参数,出现二义性
}

构造函数语法:类名()

  • 构造函数,没有返回值也不写void
  • 函数名称与类名相同
  • 构造函数有参数,因此可以发生重载
  • 程序在调用对象时会自动调用构造,无需手动调用,而且只会调用一次

构造函数的分类及调用

两种分类方式:

​ 按参数分为:有参构造和无参构造(默认构造)

​ 按类型分为:普通构造和拷贝构造

三种调用方式:

1.括号法:
1
2
3
4
5
Person p1;//默认构造函数调用
//注意1:调用默认构造函数时候,不要加(),因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
//Person p1();
Person p2(10);//有参构造函数
Person p3(p2);//拷贝构造函数
2.显示法:
1
2
3
4
5
6
7
Person p1;
Person p2=Person(10);//有参构造
Person p3=Person(p2);//拷贝构造

Person(10);//匿名对象 特点:当前执行结束后,系统会立即回收掉匿名对象
//注意2:不要利用拷贝构造函数初始化匿名对象 编译器会认为 Person(p3)==Person p3;
//Person(p3);
3.隐式转换法:
1
2
Person p4=10;//相当于 写了 Person p4=Person(10);  有参构造
Person p5=p4;//拷贝构造

C++中拷贝构造函数调用时机通常有三种:

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Person
{
public:
Person()
{
cout<<"Person默认构造函数调用"<<endl;
}
Person(int age)
{
cout<<"Person有参函数调用"<<endl;
m_age=age;
}

Person(const Person &p)
{
cout<<"Person拷贝构造函数调用"<<endl;
m_age=p.m_age;
}

~Person(int age)
{
cout<<"Person析构函数调用"<<endl;
}
int m_age;
};


//1.使用一个已经创建完毕的对象来初始化一个新对象
void text01()
{
Person p1(20);
Person p2(p1);
cout<<"p2的年龄为: "<<p2.m_age<<endl;
}
//2.对象以值传递的方式给函数参数传值
void dowork(Person p)//相当于传递一个副本,需要调用拷贝构造函数
{

}
void text02()
{
Person p;
dowork(p);
}
//3.对象以值方式返回局部对象
Person dowork2()
{
Person p1;//因为局部变量的特点作用域只在这个大括号中,所以返回局部变量时会产生一个副本代替局部变量返回到主函数
cout<<(int*)&p1<<endl;
return p1;
}
void text03()
{
Person p=dowork2();
cout<<(int*)&p<<endl;
}



main()
{
text01();
/* Person有参函数调用
Person拷贝构造函数调用
p2的年龄为:20
Person析构函数调用
Person析构函数调用
*/
text02();
/*Person默认构造函数调用
Person拷贝构造函数调用
Person析构函数调用
Person析构函数调用
*/
text03();
/*Person默认构造函数调用
010FF94
Person拷贝构造函数调用
Person析构函数调用
010FF98C
Person析构函数调用
*/
}

注意:(1)在使用对象赋值语句进行对象赋值时,两个对象的类型必须相同,如对象的类型不同,编译时将出错。 (2)两个对象之间的赋值,仅仅是对其中的数据成员赋值,而不对成员函数赋值。 (3)数据成员是占存储空间的,不同对象的数据成员占有不同的存储空间,而不同对象的成员函数是占有同一个函数代码段,无法对它们赋值。

析构函数语法:~类名()

  • 析构函数,没有返回值也不写void

  • 函数名称与类名相同,在名称前加上符号~

  • 析构函数不可以有参数,因此不能发生重载

  • 程序在对象销毁前会自动调用析构,无需手动调用且只会调用一次

  • 在以下情况, 当对象的生命周期结束时,析构函数会被自动调用:

    ① 如果定义了一个全局对象,则在程序流程离开其作用域时(如main函数结束或调用exit函数)时,调用该全局对象的析构函数。

    ② 如果一个对象被定义在一个函数体内,则当这个函数结束时,该对象的析构函数被自动调用。

    ③ 若一个对象是使用new运算符动态创建的,在使用delete运算符释放它时,delete会自动调用析构函数。

默认情况下,C++编译器至少给一个类添加三个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行值拷贝

1.构造函数的调用规则(上3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person()
{
public:
Person() //1
{
cout<<"Person的默认构造函数调用"<<endl;
}
Person(int age) //2
{
cout<<"Person的有参构造函数调用"<<endl;
m_age=age;
}
Person(const Person &p) //3
{
cout<<"Person的拷贝构造函数调用"<<endl;
m_age=p.m_age;
}
};

2.如果我们写了有参构造函数,编译器就不会再提供默认构造,依然提供拷贝构造 如果我们写了拷贝构造函数,编译器就不提供其他构造函数

深拷贝和浅拷贝问题

浅拷贝:简单的赋值拷贝操作(带来的问题是内存会重复释放),如果利用编译器提供的拷贝构造函数,就会进行浅拷贝工作

深拷贝:在堆区重新申请空间,进行拷贝操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Person()
{
public:
Person()
{
cout<<"Person的默认构造函数调用"<<endl;
}
Person(int age,int height)
{
m_age=age;
m_Height=new int(height);//利用new把数据创建在堆区,返回的数据是int*
cout<<"Person的有参构造函数调用"<<endl;
}

//自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person &p)
{
cout<<"Person拷贝构造函数调用"<<endl;
m_age=age;
//m_Height=p.m_Height; 编译器默认就是实现这一行代码(浅拷贝)
//深拷贝操作

m_Height=new int(*p.m_Height);
}


~Person()
{
//析构代码,将堆区开辟数据做释放操作
if(m_Height!=NULL)
{
delete m_Height;
m_Height=NULL;
}
}
int m_age;
int *m_Height;
};

void text01()//先进后出原则,p1后调用析构函数,后释放内存,p2先释放
{
Person p1(18,160);
cout<<"p1的年龄为:"<<p1.m_age<<"身高为:"<<*p1.m_Height<<endl;
Person p2(p1);
cout<<"p2的年龄为:"<<p2.m_age<<"身高为:"<<*p2.m_Height<<endl;
}
void main()
{
text01();
}

注意:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

308b631d9cd744ac84fde08b0e7cb024

初始化列表赋初值

语法:构造函数():属性1(值1):属性2(值2):属性3(值3)...

类对象作为类成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//手机类
class Phone
{
public:
Phone(string pName)
{
cout<<"Phone的构造函数调用"<<endl;
m_PName=pName;
}
~Person()
{
cout<<"Phone的析构函数调用"<<endl;
}
string m_PName;
};
//人类
class Person
{
public:
//m_Phone=pName 隐式转换法
Person(string name,string pName):m_Name(name),m_Phone(pName)//初始化列表的方式赋初值
{
cout<<"Person的构造函数调用"<<endl;
}
~Person()
{
cout<<"Person的析构函数调用"<<endl;
}
//姓名
string m_Name;
//手机
Phone m_Phone;
};

//当其他类对象(集合)作为本类成员(成员变量和成员函数),构造时候先构造类对象,再构造自身,析构函数的调用和构造函数的调用相反
void text01
{
Person p("张三","苹果MAX");

cout<<p.m_Name<<"拿着:"<<p.m_Phone.m_PName<<endl;
}
main()
{
text01;
}

静态成员(static)

  • 静态成员变量
    • 所有对象共享一份数据
    • 在编译阶段分配内存(全局区)
    • 类内声明,类外初始化(必须有初始值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Person
{
public:
static int m_A;//类内声明

//静态成员变量也是有访问权限的
private:
static int m_B;
};
//类外初始化
int Person::m_A=100;
int Person::m_B=200 ;

void text01()
{
Person p;
cout<<p.m_A<<endl;
Person p2;
p2.m_A=200;

cout<<p.m_A<<endl;//200 说明所有对象共享一份数据
}

void text02()
{
//静态成员变量不属于某个对象上,所有对象都共享同一份数据
//因此静态成员变量有两种访问方式

//1,通过对象进行方式
Person p;
cout<<p.m_A<<endl;

//2,通过类名进行访问 Perxon作用域下的m_A
cout<<Person::m_A<<endl;
cout<<Person::m_A<<endl;//会报错,私有同样不可以访问静态成员变量
}

main()
{
text01();
text02();
}
  • 静态成员函数
    • 所有对象共享一个函数
    • 静态成员函数只能访问静态成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person
{
public:
//静态成员函数
static void func()
{
m_A=100;//静态成员函数可以访问静态成员变量
//m_B=200; 静态成员函数不能访问非静态成员变量 无法区分到底是哪个对象的m_B
cout<<"static void func调用"<<endl;
}
static int m_A;//静态成员变量
int m_B;//非静态成员变量

//静态成员函数也有访问权限
private:
static void func2()
{
cout<<"static func2()的访问调用"<<endl;
}
};
int Person::m_A=0;


//有两种访问方式
void text01()
{
//1.通过对象访问
Person p;
p.func();
//2.通过类名访问,因为静态成员函数不属于某个对象,大家共享这个静态函数
Person::func();

//Person::func2(); 会报错因为类外不可以访问私有静态成员函数
}

int main()
{
return 0;
}

C++对象模型

成员变量和成员函数分开存储(只有非静态成员变量才属于类的对象上,非静态成员函数;静态成员变量;静态成员函数都是分开存储,不在类的对象上)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person
{
int m_A;//非静态成员变量 4 也侧面验证了非静态成员变量,属于类的对象

static int m_B;//静态成员变量 4 加上静态成员变量 类的对象所占内存还是4,侧面验证了静态成员变量不属于类对象上

void func(){} //非静态成员函数 4 不属于类对象上

static void func2(){} //4 静态成员函数 不属于类的对象上
};
int Person::m_B=0;

void text01()
{
Person p;
//空对象占用内存空间为: 1个字节
//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
//每个空对象也应该有一个独一无二的内存地址
cout<<"size of p="<<sizeof(p)<<endl;
}

void text02()//当有一个非静态成员变量时 类的对象所占内存空间为:
{
Person p;
cout<<"size of p="<<sizeof(p)<<endl; //4

}

int main()
{
text01();
text02();
}

this指针概念(不同对象的数据成员存放在不同的内存地址,所有对象的成员函数对应的是同一个函数代码段)

this指针的本质是指针常量 指针的指向是不可以修改的 但是指向的值是可以修改的

每个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,那这一块代码是如何区分是哪个对象调用自己呢?当一个对象要调用成员函数时,this指针中就装着该对象的地址, 成员函数就根据这个指针,找到相应的数据,然而进行相应的操作。
C++通过提供特殊的对象指针,this指针,解决上述问题,this指针指向被调用的成员函数所属的对象 this指针是隐含在每一个非静态成员函数的一种指针,不需要定义直接使用即可
  • 解决名称冲突
  • 返回对象本身用*this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Person
{
public:
Person(int age)
{
//this->age=age; this指针指向的是被调用的成员函数所属的对象,这里this指向p1
age=age;
}

Person& PersonAddAge(Person &p) //返回p2本体用引用方式返回,会一直返回p2,但是如果用值的方式返回就会一直调用赋值构造函数,返回的值就不是p2
{
this->age+=p.age;

//this指向p2指针,*this指向的是p2本体
return *this;

}
int age;
};

//1.解决名称冲突
void text01()
{
Person p1(18);
cout<<"p1的年龄为 "<<p1.age<<endl; //26328629 乱码出错
}
//2.返回对象本身用*this
void text02()
{
Person p1(10);
Person p2(10);
// p2.PersonAddAge(p1);
// cout<<"p2的年龄为 "<<p2.age<<endl;//20

//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
cout<<"p2的年龄为 "<<p2.age<<endl; //40
}
main()
{
text01;
}

空指针访问成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person()
{
void showClassName()
{
cout<<"this is Person class"<<endl;
}
void showPersonAge()
{
//报错原因是因为传入指针为空,无中生有
cout<<"age="<<this->m_Age<<endl;

//提高指针健壮性
if(this==NULL)
{
return;
}
}
int m_Age;
}

void text01()
{
Person *p=NULL;
p->showClassName();
// p->showPersonAge(); //读取访问权限冲突
}

main()
{
text01();
}

const修饰成员函数(常函数)

常函数

  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常含数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Person()
{
void showPerson() const //如果在成员函数形参列表后加const,其本质相当于const Person *const this,这个时候不仅指针指向不可以改,其指针指向的值也不可以修改
{
//this->m_A=100;
//this=NULL; //this指针不可以修改指针的指向的
//this指针的本质是指针常量,指针指向不可以修改,但是指针指向的值可以修改,其本质是Person * const this;

this->m_B=100;
}
int m_A;
mutable int m_B;//特殊变量,即使在常含数中,也可以修改这个值
};


void test01()
{
Person p;
p.showPerson();
}


void test02()
{
const Person p;//对象前加const,变为常对象
//p.m_A=100;//不允许修改 不允许更改常对象的数据成员 不允许常对象调用普通的成员函数
p.m_B=200;//允许修改

//常对象只能调用常含数
p.showPerson();
//p.func();//会报错
}

main()
{
text01();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>    //如果在一个类中说明了常数据成员,那么构造函数就只能通过初始化列表对该数据成员进行初始化,而不能采用在函数中直接赋值的方法。
using namespace std;
class Date {
private:
const int year;
const int month;
const int day;
public:
Date(int y,int m,int d);
void showDate(); };
Date::Date(int y,int m,int d) :year(y),month(m),day(d) //只能通过初始化列表对该数据成员进行初始化
/* //cui
如果构造函数改写成:
Date::Date(int y,int m,int d)
{ year=y;
month=m;
day=d; }
*/
{ }
inline void Date::showDate()
{ cout<<year<<"."<<month<<"."<<day<<endl;}
int main()
{ Date date1(2009,10,15);
date1.showDate(); return 0; }

友元

  • 全局函数做友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//建筑物类
class Building
{
//全局函数的声明,goodGay全局函数是Building好朋友,可以访问私有类成员
friend void goodGay(Building *building);

public:
Building()
{
m_SittingRoom="客厅";
m_BedRoom="卧室";
}
public:
string m_SittingRoom;//客厅
private:
string m_BedRoom;//卧室
};
//全局函数
void goodGay(Building *building)
{
cout<<"好基友全局函数 正在访问:"<<building->m_SettingRoom<<endl;

cout<<"好基友全局函数 正在访问:"<<building->m_BedRoom<<endl;
}

void text01()
{
Building building;
goodGay(&building);
}

main()
{
text01();
}
  • 类做友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Building;
class GoodGay
{
public:
GoodGay();

void visit();//参观函数 访问Building中的属性

Building *building;
};

class Building
{
friend class GoodGay;//GoodGay这个类是本类的好朋友,可以访问私有
public:
Building(); //构造函数声明

public:
string m_SittingRoom;//客厅
private:
string m_BedRoom;//卧室
}

//类外给构造函数或成员函数赋初值(类内也可以)
Building::Building()
{
m_SettingRoom="客厅";
m_BedRoom="卧室";
}

GoodGay::GoodGay()
{
//创建建筑物的对象
building=new Building; //new 在堆区创造一个对象
}

void GoodGay::visit()
{
cout<<"好基友类正在访问:"<<building->SettingRoom<<endl;
}


void text01()
{
GoodGay gg;
gg.visit();
}

void main()
{
text01();
}
  • 成员函数做友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Building;

//GoodGay类
class GoodGay()
{
public:
GoodGay();

void visit(); //让visit函数可以访问Building中私有成员
void visit2(); //让visit2函数不可以访问Building中私有成员

Building *building;
};

//Building类
class Building
{
//告诉编译器 GoodGay类下的visit成员函数可以访问Building类的私有成员
friend void GoodGay::visit();
public:
Building();
public:
string m_SittingRoom;//客厅
private:
string m_BedRoom;
}

//类外实现成员函数
Building::Building()
{
m_SittingRoom="客厅";
m_BedRoom="卧室";
}
GoodGay::GoodGay()
{
building=new Building:
}

void GoodGay::visit()
{
cout<<"visit正在访问:"<<building->m_SettingRoom<<endl;
cout<<"visit正在访问:"<<building->m_BedRoom<<endl;
}

void GoodGay::visit2()
{
cout<<"visit2正在访问:"<<building->m_SettingRoom<<endl;
// cout<<"visit2正在访问:"<<building->m_BedRoom<<endl;
}

void text01()
{
GoodGay gg;
gg.visit();//客厅 卧室都可以访问
gg.visit2();//客厅可以 卧室不可以
}

void main()
{

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
include<iostream>
using namespace std;
class Date; //对Date类的提前引用声明
class Time{ //声明类Time
public:
Time (int h,int m,int s) //定义构造函数
{ hour =h; minute =m; sec =s;}
void showDate_Time(Date&); //声明函数showDate_Time为类Time的成员函数,也是类Date的友元函数
private:
int hour; int minute; int sec; };
class Date{ //声明类Date
public:
Date(int y,int m,int d) //定义构造函数
{ year=y; month=m; day=d; }
friend void Time::showDate_Time(Date&);
private:
int year; int month; int day;};

void Time::showDate_Time (Date& d) //定义函数showDate_Time为类Time的成员函数,也是类Date的友元函数,形参为Date类对象的引用
{
cout<<d.year<<"."<<d.month<<"."<<d.day<<endl; //函数showDate_Time作为Time类的成员函数,可以访问Time类对象中的私有数据

cout<<hour<<":"<<minute<<":"<<sec<<endl; //函数showDate_Time作为Date类的友元函数,可以访问Date类对象中的私有数据

}

int main()
{
Date date1(2010,11,14);//定义Date类对象date1
Time time1(6,12,18); //定义Time类对象time1
time1.showDate_Time(date1); return 0; //调用函数showDate_Time,实参是Date类对象date1
}


程序的运行结果如下:
2010.11.14
6:12:18
(1) 友元关系是单向的,不具有交换性。

若类X是类Y的友元,类Y是否是X的友元,要看在类中是否有相应的声明。

(2) 友元关系也不具有传递性。

若类X是类Y的友元,类Y是类Z的友元,不一定类X是类Z的友元。

运算符重载

对已有的运算符进行定义,赋予其另一种功能,以适应不同的数据类型

加号运算符重载(实现两个自定义数据类型的运算)

  • 对于内置的数据类型的表达式的运算符是不可能改变的
  • 不要滥用运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//使p1的m_A属性加上p2的m_A属性等于p3的m_A属性;使p1的m_B属性加上p2的m_B属性等于p3的m_B属性
class Person
{
public:

Person operator+(Person &p)
{
Person temp;
temp.m_A=this->m_A+p.m_A;
temp.m_B=this->m_B+p.m_B;
return temp;
}

int m_A;
int m_B;
};
void test01()
{
Person p1:
p1.m_A=10;
P1.m_B=10;

Person p2:
p2.m_A=10;
P2.m_B=10;

Person p3=p1+p2;
cout<<"p3.m_A"<<p3.m_A<<endl;
cout<<"p3.m_B"<<p3.m_B<<endl;
}
main()
{
text01();
}


/*可以通过自己写成员函数来实现两个对象相加后返回新的对象
Person PersonAddPerson(Person &p)
{
Person temp;
temp.m_A=this->m_A+p.m_A;
temp.m_B=this->m_B+p.m_B;
return temp;
}
*/

//这个时候编译器起了一个通用名称
// operator+
//这个时候成员函数就可以变为
Person operator+(Person &p)
{
Person temp;
temp.m_A=this->m_A+p.m_A;
temp.m_B=this->m_B+p.m_B;
return temp;
}

//实现上诉目的就的代码为
Person p3=p1.operator+(p2);
//简化为
Person p3=p1+p2;


2.
/*通过全局函数重载+
Person operator+(Person &p1,Person &p2)
{
person temp;
temp.m_A=p1.m_A+p2.m_A;
temp.m_B=p1.m_B+p2.m_B;
return temp;
}

//调用这个全局函数
Person p3=operator+(p1,p2)
//简化为
Person p3=p1+p2;
*/


运算符重载 也可以发生函数重载(用途:重复使用函数名)
1
2
3
4
5
6
7
8
9
10
11
12
Person p3=p1+10;  //Person + int
//函数重载版本
Person Person+(Person &p1,int)
{
person temp;
temp.m_A=p1.m_A+p2.m_A;
temp.m_B=p1.m_B+p2.m_B;
return temp;
}
Person p4=p1+100;
cout<<"p4.m_A"<<p4.m_A<<endl;
cout<<"p4.m_B"<<p4.m_B<<endl;

左移运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//为了输出p时就能将其中的所有属性都输出
class Person()
{
friend ostream & operator<<(cout,p);
public:
//利用成员函数重载左移运算符 p.operator<<(cout) 简化版本 p<<cout
//不会利用成员函数重载<<运算符,因为无法实现cout在左侧
//void operator<<(cout) 在不知道返回值的情况下,先用void


public:
//在构造函数中赋初值
Person(int a,int b)
{
m_A=a;
m_B=b;
}


int m_A;
int m_B;
};

//只能利用全局函数重载左移运算符
ostream & operator<<(ostream &cout,Person &p) //本质 operator<<(cout) 简化为cout<<p; cout本质为标准输出流对象,全局只能有一个
{
cout<<"m_A="<<p.m_A<<"m_B="<<p.m_B;
return cout; //cout可以改成任意别名
}


void text01()
{
Person p(10,10);

//cout<<p<<endl; 没有与这些操作数匹配的"<<"运算符
//全局函数编码成功后就可以输出p
//链式反应
cout<<p<<"hello"<<endl;
}

int main()
{
text01();
return 0;
}

递增运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class MyInteger
{
public:
friend ostream& operator<<(ostream& cout,Myinteger myint)
MyInteger()
{
m_Num=0
}

//重载++运算符(前置)
MyInteger& operator++() //为什么返回不能返回值而是返回引用的数据?为了多次累加的链式反应不出错,为了对同一个数据做递增操作
{
m_Num++;
return *this;
}

//重载++运算符(后置)
//因为temp是一个局部对象,在这个函数执行完就会被释放,如果还要返回它的引用,之后就是非法操作,所以后置返回的是值的形式
MyInteger operator++(int) //int代表占位参数,用于区分前置和后置递增
{
//先 记录当时结果
MyInteger temp=*this;
//后 递增
m_Num++;
//最后将记录结果返回
return temp;
}

private:
int m_Num;
};

//重载左移运算符
ostream& operator<<(ostream& cout,Myinteger myint)
{
cout<<myint.m_Num;
return cout;
}

void text01()
{
MyInteger myint;
cout<<++(++myint)<<endl; //用引用或者值返回都为2
cout<<myint<<endl; //此时用引用返回的为2,而用值返回的就为1
}

void text02()
{
MyInteger myint;

cout<<myint++<<endl;
cout<<myint<<endl;

}

int main()
{
text01();
text02();
}

赋值运算符重载

编译器会至少给一个类添加4个函数
  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行拷贝
  • 赋值运算符operator=对属性进行值拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Person
{
Person(int age)
{
m_Age=new int(age); //new运算符生成一个堆区数据,由程序员手动开辟,也需要他手动释放(手动释放用析构函数),返回类型为int*
}
~Person()
{
if(m_Age!=NULL)
{
delete m_Age;
m_Age=NULL;
}
}

//重载 赋值运算符
Person& operator=(Person &p)
{
//编译器是提供浅拷贝
//m_Age=p.m_Age;

//1.应该先判断是否有属性在堆区,如果有要先释放干净,然后再深拷贝
if(m_Age!=NULL)
{
delete m_Age;
m_Age=NULL;
}
//2.深拷贝
m_Age=new int(*p.m_Age)

//3.返回对象本身
return *this;
}

int *m_Age;
};

void text01()
{
Person p1(18);

Person p2(20);

Person p3(30);
//赋值运算操作 用编译器提供的赋值操作,只是单纯把p1中的年龄的地址值赋值给p2,会出现重复释放的问题,所以要自己写一个赋值运算符
p3=p2=p1;

cout<<"p1的年龄为:"<<*p1.m_Age<<endl; //18
cout<<"p2的年龄为:"<<*p2.m_Age<<endl; //18
cout<<"p3的年龄为:"<<*p3.m_Age<<endl; //18

}


int main()
{
text01();
}

关系运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Person
{
public:
Person(string name,int age)
{
m_Name=name;
m_Age=age;
}
//重载关系运算符
bool operator==(Person &p)
{
if(this->m_Name==p.m_Name && this->m_Age==p.m_Age)
{
return true;
}
return false;
}


string m_Name;
int m_Age;
};
void test01()
{
Person p1("Tom,18");

Person p2("Tom,18");
if(p1==p2)
{
cout<<"p1和p2是相等的"<<endl;
}
else
{
cout<<"p1和p2是不相等的"<<endl;
}
}

int main()
{
test01();
}

调用运算符重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Myprint
{
public:
//重载函数调用运算符
void operator()(string test)
{
cout<<test<<endl;
}
};

void MyPrint02(string test)
{
cout<<test<<endl;
}

void test01()
{
MyPrint myPrint;

myPrint("hello world");//对象使用重载调用运算符

MyPrint02("hello world");//调用函数
}
//加法函数
class MyAdd
{
public:
int operator()(int num1,int num2)
{
return num1+num2;
}
};
void test02()
{
MyAdd myadd;
int ret=myadd(100,100);
cout<<"ret="<<ret<<endl;

//匿名函数对象
cout<<MyAdd()(100,100)<<endl; //匿名对象重载
}

int main()
{
test01();
test02(); //仿函数没有固定写法,很灵活,会根据我们的需求改变返回值类型和参数类型和个数

return 0;
}

继承

语法: class 子类(派生类) :继承方式 父类(基类)

  • 公共继承
  • 保护继承
  • 私有继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//公共继承
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};


class Son1:public Base1
{
public:
void func()
{
m_A=10;//父类中的公共权限成员 在子类依然是公共权限
m_B=20;//父类中的保护权限成员 在子类依然是保护权限
// m_C=30; 父类中的私有权限成员 子类访问不到
}
};

void text01()
{
Son1 s1;
s1.m_A=100;
// s1.m_B=100; //保护权限类内可以访问,类外不可以访问
}

class Base2
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
//保护继承
class Son2:protected Base2
{
public:
void func()
{
m_A=100; //父类中公共成员 在子类中变成保护权限
m_B=100; //父类中保护成员 依然在子类为保护权限
// m_C=100; //父类中的私有权限成员 子类访问不到
}
}
void text02()
{
Son2 s1;
// s1.m_A=100; 保护权限类内可以访问,类外不可以访问
// S1.m_B=100; 保护权限类内可以访问,类外不可以访问
// s1.m_C=100; 保护权限类内可以访问,类外不可以访问
}

class Base3
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
//私有继承
class Son3:private Base3
{
public:
void func()
{
m_A=100; //父类中公共成员 在子类中变为私有成员
m_B=100; //父类中保护成员 在子类中变为私有成员
m_c=100; //父类中私有成员 子类访问不到
}
};

void text03()
{
Son s1;
s1.m_A=1000; //私有成员类外访问不到
S2.m_B=1000; //私有成员类外访问不到
}

继承中的对象模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};

class Son:public Base
{
public:
int m_D;
};

void text01()
{
cout<<"size of Son:"<<sizeof(Son)<<endl; //16 父类中所有非静态成员属性成员,子类都会继承,父类中私有成员属性,是被编译器隐藏了,子类访问不到,但是确实被继承了
}

int main()
{
text01();
}

当基类含有带参数的构造函数时,派生类必须定义构造函数,并缀上基类名(参数表),以提供把参数传递给基类构造函数的途径。其中基类构造函数参数表的参数,通常来源于派生类构造函数的参数表,也可以用常数值。

(1)可以将派生类构造函数定义在类的外部,而在类体内只写该函数的声明。

1
2
3
4
5
6
7
8
9
//在派生类中声明构造函数的原型:
D(int a,int b); //在此,不包括基类构造函数名及其参数表(即:base(b))

//而在类的外部定义派生类的构造函数:
D(int a,int b):B(b) //在此,要列出基类构造函数名及其参数表(即:base(b))
{ cout<<"调用派生类的构造函数\n"<<endl;
j=a;
}

当派生类中含有对象成员时,其构造函数的一般形式为:

派生类名(参数总表基类名(参数表0),对象名成员1(参数表1),…,对象成员名n (参数表n)

{

//派生类新增成员的初始化语句 ..

}

继承中构造和析构顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Base
{
public:
Base()
{
cout<<"Base的构造函数"<<endl;
}
~Base()
{
cout<<"Base的析构函数"<<endl;
}
};
class Son:public Base
{
public:
Son()
{
cout<<"Son的构造函数"<<endl;
}
~Son()
{
cout<<"Son的析构函数"<<endl;
}
};

void text01()
{
// Base b; Base的构造函数 Base的析构函数

//先构造父类再构造子类,析构和构造的顺序相反
Son s; // Base的构造函数 Son的构造函数 Son的析构函数 Base的析构函数
}

main()
{
text01();
}

继承中同名成员的处理方式

当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据

  • 访问子类同名成员 直接访问就行
  • 访问父类同名成员 需要加作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Base
{
public:
Base()
{
m_A=100;
}
int m_A;

void func(int a)
{
cout<<"Base-func()调用"<<endl;
}
};
class Son:public Base
{
public:
Son()
{
m_A=200;
}
void func()
{
cout<<"Son-func()调用"<<endl;
}
int m_A;
};
//同名数据成员,通过子类对象的处理方式
void text01()
{
Son s;
cout<<"Son下 m_A="<<s.m_A<<endl; //200,直接访问访问的是同名中的子类
//如果通过子类对象 访问到父类中同名成员,需要加作用域
cout<<"Base下 m_A="<<s.Base::m_A<<endl;
}


//同名成员函数处理方式
void text02()
{
Son s;
s.func(); //Son-func()调用 直接调用,调用的是子类中的成员函数
//如何调用到父类中同名成员函数?
s.Base::func(); //Base-func()调用


//如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数 所以s.func(100); 会报错
//如果想访问父类中被隐藏的同名成员函数需要加作用域
s.Base::func(100);
}

main()
{
text01();
text02();
}

继承中同名静态成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Base
{
public:
static int m_A;

static void func()
{
cout<<"Base static void func()"<<endl;
}

static void func(int a)
{
cout<<"Base static void func(int a)"<<endl;
}
};
int Base::m_A=100;

class Son::public Base
{
public:
static int m_A;
static void func()
{
cout<<"Son static void func()"<<endl;
}
};
int Son::m_A=200;

void text01()
{
//1.通过对象来访问数据
Son s;
cout<<"Son 下 m_A="<<s.m_A<<endl; //100
cout<<"Base 下 m_A="<<Son::m_A<<endl; //200

//2.通过类名方式访问
cout<<"通过类名访问:"<<endl;
cout<<"Son 下 m_A="<<Son::m_A<<endl;
cout<<"Base 下 m_A="<<Son::Base::m_A<<endl;//子类通过域名访问父类作用域 下的m_A
}

//同名静态成员函数
void text01()
{
//1.通过对象访问
cout<<"通过对象访问"<<endl;
Son s;
s.func();
s.Base::func();

//2.通过类名访问
cout<<"通过类名访问"<<endl;
Son::func();
Son::Base::func();

//Son::func(100) 这时也无法通过函数重载调用带参的函数 因为子类出现和父类同名成员函数,会隐藏父类中所有同名成员函数,这时候还是需要加作用域才能访问
Son::Base::func();
}

多继承语法

语法:class 子类:继承方式 父类1,继承方式 父亲2...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Base1
{
public:
Base1()
{
m_A=100;
}
int m_A;
};
class Base2
{
public:
Base2()
{
m_B=200;
}
int m_B;
};

//子类 需要继承Base1和Base2
class Son:public Base1,public Base2
{
public:
Son()
{
m_C=300;
m_D=400;
}
};
void text01()
{
Son s;
cout<<"sizeof Son="<<sizeof(s)<<endl; //16

// cout<<"m_A="<<s.m_A<<endl; 二义性,当父类中出现同名成员,需要加作用域
cout<<"Base1 m_A="<<Base1::s.m_A<<endl; //100
cout<<"Base2 m_A="<<Base2::s.m_A<<endl; //200
//通常在开发过程中不做多继承
}

int main()
{
text01();
}

虚基类(菱形继承)

  • 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时就会产生二义性
  • 草泥马继承了动物的数据继承了两份,但这个数据继承一次就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//动物类
class Animal
{
public:
int m_Age;
};
//利用虚继承 解决菱形继承的问题
//继承之前 加上关键字virtual变为虚继承
//Animal类称为虚基类
//羊类
class Sheep:virtual public Animal
{

};
//驼类
class Tuo:virtual public Animal
{

};
//羊驼类
class SheepTuo:public Sheep,public Tuo
{

};

void text01()
{
SheepTuo st;
// st.m_Age=18; 这时候就会出现多继承的问题,产生二义性
st.Sheep::m_Age=18;
st.Tuo::m_Age=28;
cout<<" st.Sheep::m_Age="<< st.Sheep::m_Age<<endl;
cout<<" st.Tuo::m_Age="<< st.Tuo::m_Age<<endl;

//当成为虚继承时,数据就变成了一个这个时候不会出现二义性,先把m_Age赋值为18,再赋值为28
cout<<" st.m_Age="<< st.m_Age<<endl; //28

}

int main()
{
text01();
}
派生类对基类成员的访问形式主要有以下两种:

内部访问 由派生类中新增的成员函数 对基类继承来的成员的访问

对象访问 在派生类外部通过派生类的对象对从基类继承来的成员的访问

屏幕截图_20230511_110042
屏幕截图_20230511_110227
屏幕截图_20230511_110626
对于基类中的私有成员:

无论哪种派生方式,基类中的私有成员,

不允许派生类的对象直接访问(对象访问),

不允许派生类中成员函数直接访问(内部访问) ,

但是可以通过基类提供的公有成员函数访问。

多态

  • 静态多态:函数重载和运算符重载属于静态多态,重复使用函数名
  • 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
  • 静态多态的函数地址早绑定,编译阶段确定函数地址
  • 动态多态的函数地址晚绑定,运行阶段确定函数地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//动物类
class Animal
{
public:
//虚函数
virtual void speak()
{
cout<<"动物在说话"<<endl;
}
};
//猫类
class Cat:public Animal
{
public:
void speak()
{
cout<<"小猫在说话"<<endl;
}
};

//执行说话的函数
//地址早绑定,在编译阶段就确定函数地址
void doSpeak(Animal &animal) //在用父类的引用接收子类的对象 Animal & animal=cat 在C++中允许父子之间的类型转换,不需要做强制类型转换,父类的引用或者指针可以直接指向子类对象
//现在的问题是speak()这个函数是走猫还是动物? 我们本意是想让猫说话,但是执行结果是动物在说话 如果想执行让猫说话,就不能让这个函数地址提前绑定,需要在运行阶段进行绑定
{
animal.speak();
}

void text01()
{
Cat cat;
doSpeak(cat);
}

void text02()
{
cout<<"sizeof Animal="<<sizeof(Animal)<<endl; //在没有构建虚函数前因为成员变量和成员函数分开存储(只有非静态成员变量才属于类的对象上,非静态成员函数;静态成员变量;静态成员函数都是分开存储,不在类的对象上),所以现在的Animal类相当于一个空类,空类的大小是'1'`
cout<<"sizeof Animal="<<sizeof(Animal)<<endl; //在构建完虚函数后结果变成了'4'(不管什么类型的指针都占四个字节)
}
int main()
{
text01();
text02();
}
在C++中规定: 基类的对象指针可以指向它的公有派生的对象,但是当其指向公有派生类对象时,它只能访问派生类中从基类继承来的成员,而不能访问公有派生类中定义的成员
1
2
3
4
5
6
7
8
9
10
11
12
class A {         
publicvoid print1()
{. . .} };
class Bpublic A {
publicvoid print2(){. . .} };
void main()
{A *p1; //定义基类A的指针变量p1
B op2; //定义派生类B的对象op2
p1=&op2;//将指针变量p1指向派生类对象op2
p1->print1(); //正确, 基类指针变量p1可以访问派生类中从基类继承来的成员函数print1()
p1->print2(); //错误,基类指针变量p1不能访问派生类中定义的成员函数print2()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<iostream.h>
class base { //例 虚函数引例
int a,b;
public:
base(int x,int y) { a=x; b=y; }
void show( )
{ cout<<"调用基类base的show函数\n";
cout<<"a=" <<a<<“b="<<b<<endl; } };
class dirive:public base {
int c;
public:
dirive(int x,int y,int z):base(x,y){c=z;}
void show( ) { cout<< "调用派生类dirive的show函数\n";
cout<<"c="<<c<<endl; } };
void main( )
{
base mb(50,50),*mp;
dirive mc(10,20,30);
mp=&mb;
mp->show( );
mp=&mc;
mp->show( );
}

/*
程序运行结果不是:
调用基类base的show函数
a=50 b=50
调用派生类dirive的show函数
c=30

程序运行结果如下:
调用基类base的show函数
a=50 b=50
调用基类base的show函数
a=10 b=20
*/

如果在派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。

1
2
3
4
5
6
7
8
class B0{
. . .
public:
virtual void show(){. . .} //在基类B0中,定义show为虚函数
};
class B1:public B0{
... //若在公有派生类B1中没有重新定义虚函数show,则函数show在派生类中被继承,仍是虚函数。
};

动态多态满足条件

1.有继承关系
2.子类要重写父类的虚函数,这时virtual可以不写 virtual void speak()和void speak()相同 重写:函数名相同,形参列表,函数返回值类型都完全相同

动态多态的使用

父类的指针或引用 指向子类对象

动态多态的原理

屏幕截图_20230409_171013

cat类中重写speak函数与否的原理(上边没有重写,下边重写了)

屏幕截图_20230409_171528
屏幕截图_20230409_171800

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
语法:virtual 返回值类型 函数名 (形参列表)=0,当类中有了纯虚函数,这个类就称为抽象类

抽象类:

  • 无法实例化对象
  • 子类必须重写抽象类(父类)中的纯虚函数,否则也属于抽象类,无法实例化对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Base
{
public:
virtual void func()=0;
};

class Son:public Base
{
public:
virtual void func()
{
cout<<"func函数调用"<<endl;
}
};

void text01()
{
//Base b; 抽象类无法实例化对象
// new Base; 抽象类无法实例化对象

Son s; //子类必须重写父类中的纯虚函数,否则无法实例化对象

Base *base=new Son;
base->func(); //func函数调用
}
int main()
{
text01();
}
多态:多态的作用就是为了使函数接口更通用化,通过一个父类的指针或引用,因为创造的对象不同,可以调用多种形态的函数

虚析构和纯虚析构

多态使用时,如果子类中有些属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

  • 虚析构函数没有类型,也没有参数。

  • 如果将基类的析构函数定义为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。

  • 在C++中,不能声明虚构造函数,但是可以声明虚析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>   //本程序只执行了基类B的析构函数,而没有执行派生类D的析构函数。原因是当撤销指针P所指的派生类的无名对象, 调用析构函数时,采用了静态联编方式,只调用了基类B的析构函数。 
using namespace std;
class B{
public:
~B()
{ cout<<"调用基类B的析构函数\n";}
};
class D:public B{
public:
~D()
{ cout<<"调用派生类D的析构函数\n";}
};
int main()
{ B *p; //定义指向基类B的指针变量p
p= new D; //运算符new为派生类的无名对象动态地 分配了一个存储空间,并将地址赋给对象指针p
delete p;   //用delete撤销无名对象,释放动态存储空间
return 0; }
//程序运行的结果为:调用基类B的析构函数

-------------------------------------------------
#include<iostream> // 虚析构函数的使用。
using namespace std;
class B{
public:
virtual ~B()
{ cout<<"调用基类B的析构函数\n";}
};
class D:public B{
public:
~D()
{ cout<<"调用派生类D的析构函数\n";}
};
int main()
{ B *p; //定义指向基类B的指针变量p
p= new D;
delete p;  
return 0; }
//程序运行的结果为:调用派生类D的析构函数调用基类B的析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Animal
{
public:
Animal()
{
cout<<"Animal构造函数调用"<<endl;
}
//利用虚析构可以解决 父类指针释放子类对象时不干净的问题
/*
virtual ~Anmial()
{
cout<<"Animal析构函数调用"<<endl;
}
*/
//纯虚析构 需要声明也需要实现
//有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
virtual ~Animal()=0; //声明
//纯虚函数
virtual void spesk()=0;
};

Animal::~Animal() //shi'xia
{
cout<<"Animal纯虚析构函数调用"<<endl;
}

class Cat:public Animal
{
public:
Cat(string name)
{
cout<<"Cat的构造函数调用"<<endl;
m_Name=new string(name);
}
void speak()
{
cout<<*m_Name<<"小猫在说话"<<endl;
}
~Cat()
{
if(m_Name!=NULL)
{
cout<<"Cat析构函数调用"<<endl;
delete m_Name;
m_Name=NULL;
}
}
string *m_Name; //将小猫的名字创建在堆区,用指针维护
};

void text01()
{
Animal* animal=new Cat; //父类指针指向子类对象 多态
animal->speak();
//父类的指针在析构时不会调用子类中析构函数,导致子类如果有堆区属性,会有泄露情况,解决办法是把Animal析构函数变成纯虚构函数
delete animal;
}

int main()
{
text01(); //Animal构造函数调用 Cat构造函数调用 Tom小猫在说话 Animal析构函数调用 此时还没有Cat析构函数调用的语句
}
  • 由于抽象类中至少包含有一个没有定义功能的纯虚函数,因此抽象类只能用作其他类的基类,不能建立抽象类对象。

  • 不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类;

  • 抽象类不能用作函数的参数类型、函数的返回类型或显式转换的类型;

  • 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。

  • 在派生类中,如果对基类的纯虚函数没有重新定义,则该函数在派生类中仍是纯虚函数,该派生类仍为抽象类。