1.c++的初识
c++ 第一个程序(必不可少 哈哈哈哈)
#include<iostream>
using namespace std;
int main(){
cout << "hello world" << endl;
return EXIT_SUCCESS;
}
解析:
- #include<iostream>; 预编译指令,引入头文件iostream.
- using namespace std; 使用标准命名空间
- cout << “hello world”<< endl; 和printf功能一样,输出字符串”hello wrold”
c++头文件为什么没有.h?
在c语言中头文件使用扩展名.h,将其作为一种通过名称标识文件类型的简单方式。但是c++得用法改变了,c++头文件没有扩展名。但是有些c语言的头文件被转换为c++的头文件,这些文件被重新命名,丢掉了扩展名.h(使之成为c++风格头文件),并在文件名称前面加上前缀c(表明来自c语言)。例如c++版本的math.h为cmath.
由于C使用不同的扩展名来表示不同文件类型,因此用一些特殊的扩展名(如hpp或hxx)表示c++的头文件也是可以的,ANSI/IOS标准委员会也认为是可以的。
头文件类型 | 约定 | 示例 | 说明 |
c++旧式风格 | 以.h结尾 | iostream.h | c++程序可用 |
c旧式风格 | 以.h结尾 | math.h | c/c++程序可用 |
c++新式风格 | 无扩展名 | iostream | c++程序可用,使用namespace std |
转换后的c | 加上前缀c,无扩展名 | Cmath | c++程序可用,可使用非c特性,如namespace std |
问题2:using namespace std 是什么?
namespace是指标识符的各种可见范围。命名空间用关键字namespace 来定义。命名空间是C++的一种机制,用来把单个标识符下的大量有逻辑联系的程序实体组合到一起。此标识符作为此组群的名字。
问题3:cout 、endl 是什么?
cout是c++中的标准输出流,endl是输出换行并刷新缓冲区。
1.1 面向过程
面向过程是一种以过程为中心的编程思想。
通过分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向过程编程思想的核心:功能分解,自顶向下,逐层细化(程序=数据结构+算法)。
面向过程编程语言存在的主要缺点是不符合人的思维习惯,而是要用计算机的思维方式去处理问题,而且面向过程编程语言重用性低,维护困难。
1.2面向对象
面向对象编程(Object-Oriented Programming)简称 OOP 技术,是开发计算机应用程序的一种新方法、新思想。过去的面向过程编程常常会导致所有的代码都包含在几个模块中,使程序难以阅读和维护。在做一些修改时常常牵一动百,使以后的开发和维护难以为继。而使用 OOP 技术,常常要使用许多代码模块,每个模块都只提供特定的功能,它们是彼此独立的,这样就增大了代码重用的几率,更加有利于软件的开发、维护和升级
在面向对象中,算法与数据结构被看做是一个整体,称作对象,现实世界中任何类的对象都具有一定的属性和操作,也总能用数据结构与算法两者合一地来描述,所以可以用下面的等式来定义对象和程序:
对象 = 算法 + 数据结构
程序 = 对象 + 对象 + ……
1.3面向对象的三大特点
- 封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态。
- 继承
继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。
继承的作用:避免公用代码的重复开发,减少代码和数据冗余。
- 多态
多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
附:作用域的作用
如果有两个同名变量,一个是全局变量,另一个是局部变量,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。
int a = 10;
void test(){
//局部变量
int a = 20;
//全局a被隐藏
cout << "a:" << a << endl; //输出的a=20;
}
作用域运算符可以用来解决局部变量与全局变量的重名问题
//全局变量
int a = 10;
//1. 局部变量和全局变量同名
void test(){
int a = 20;
//打印局部变量a
cout << "局部变量a:" << a << endl;
//打印全局变量a
cout << "全局变量a:" << ::a << endl;
}
2.命名空间
在c++中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称互相冲突性的可能性越大。另外使用多个厂商的类库时,也可能导致名称冲突。为了避免,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入关键字namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。
创建一个命名空间:
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
void test(){
cout << "A::a : " << A::a << endl;
cout << "B::a : " << B::a << endl;
}
注意:命名空间只能在全局使用
错误例子如下:
void test(){
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
cout << "A::a : " << A::a << endl;
cout << "B::a : " << B::a << endl;
}
2.1命名空间的用法
- 命名空间可嵌套命名空间
namespace A{
int a = 10;
namespace B{
int a = 20;
}
}
void test(){
cout << "A::a : " << A::a << endl;
cout << "A::B::a : " << A::B::a << endl;
}
2.命名空间是开放的,即可以随时把新的成员加入已有的命名空间中
namespace A{
int a = 10;
}
namespace A{
void func(){
cout << "hello namespace!" << endl;
}
}
void test(){
cout << "A::a : " << A::a << endl;
A::func();
}
- 声明和实现可分离
#pragma once
namespace MySpace{
void func1();
void func2(int param);
}
void MySpace::func1(){
cout << "MySpace::func1" << endl;
}
void MySpace::func2(int param){
cout << "MySpace::func2 : " << param << endl;
}
无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接
namespace{
int a = 10;
void func(){ cout << "hello namespace" << endl; }
}
void test(){
cout << "a : " << a << endl;
func();
}
命名空间别名
namespace veryLongName{
int a = 10;
void func(){ cout << "hello namespace" << endl; }
}
void test(){
namespace shortName = veryLongName;
cout << "veryLongName::a : " << shortName::a << endl;
veryLongName::func();
shortName::func();
}
2.2using声明
using声明可使得指定的标识符可用。
namespace A{
int paramA = 20;
int paramB = 30;
void funcA(){ cout << "hello funcA" << endl; }
void funcB(){ cout << "hello funcA" << endl; }
}
void test(){
//1. 通过命名空间域运算符
cout << A::paramA << endl;
A::funcA();
//2. using声明
using A::paramA;
using A::funcA;
cout << paramA << endl;
//cout << paramB << endl; //不可直接访问
funcA();
//3. 同名冲突
//int paramA = 20; //相同作用域注意同名冲突
}
using声明碰到函数重载
namespace A{
void func(){}
void func(int x){}
int func(int x,int y){}
}
void test(){
using A::func;
func();
func(10);
func(10, 20);
}
如果命名空间包含一组用相同名字重载的函数,using声明就声明了这个重载函数的所有集合。
2.3 using编译指令
using编译指令使整个命名空间标识符可用.
namespace A{
int paramA = 20;
int paramB = 30;
void funcA(){ cout << "hello funcA" << endl; }
void funcB(){ cout << "hello funcB" << endl; }
}
void test01(){
using namespace A;
cout << paramA << endl;
cout << paramB << endl;
funcA();
funcB();
//不会产生二义性
int paramA = 30;
cout << paramA << endl;
}
namespace B{
int paramA = 20;
int paramB = 30;
void funcA(){ cout << "hello funcA" << endl; }
void funcB(){ cout << "hello funcB" << endl; }
}
void test02(){
using namespace A;
using namespace B;
//二义性产生,不知道调用A还是B的paramA
//cout << paramA << endl;
}
注意:使用using声明或using编译指令会增加命名冲突的可能性。也就是说,如果有名称空间,并在代码中使用作用域解析运算符,则不会出现二义性。
附:
C++对全局变量检测增强,下面的代码在C语言编译是通过的。c++则失败
int a = 10; //赋值,当做定义
int a; //没有赋值,当做声明
int main(){
printf("a:%d\n",a);
return EXIT_SUCCESS;
}
3.c++结构体的加强
- c中定义结构体变量需要加上struct关键字,c++不需要。
- c中的结构体只能定义成员变量,不能定义成员函数。c++即可以定义成员变量,也可以定义成员函数。
//1. 结构体中即可以定义成员变量,也可以定义成员函数
struct Student{
string mName;
int mAge;
void setName(string name){ mName = name; }
void setAge(int age){ mAge = age; }
void showStudent(){
cout << "Name:" << mName << " Age:" << mAge << endl;
}
};
//2. c++中定义结构体变量不需要加struct关键字
void test01(){
Student student;
student.setName("John");
student.setAge(20);
student.showStudent();
}
4.新增bool关键字类型
标准c++的bool类型有两种内建的常量true(转换为整数1)和false(转换为整数0)表示状态。这三个名字都是关键字。
- bool类型只有两个值,true(1值),false(0值)
- bool类型占1个字节大小
- 给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换false(0)
void test()
{ cout << sizeof(false) << endl; //为1,//bool类型占一个字节大小
bool flag = true; // c语言中没有这种类型
flag = 100; //给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换false(0)
}
[c语言中的bool类型]
c语言中也有bool类型,在c99标准之前是没有bool关键字,c99标准已经有bool类型,包含头文件stdbool.h,就可以使用和c++一样的bool类型。
5.三目运算的增强
- c语言三目运算表达式返回值为数据值,为右值,不能赋值。
int a = 10;
int b = 20;
printf("ret:%d\n", a > b ? a : b);
//思考一个问题,(a > b ? a : b) 三目运算表达式返回的是什么?
//(a > b ? a : b) = 100;
//返回的是右值
- c++语言三目运算表达式返回值为变量本身(引用),为左值,可以赋值。
int a = 10;
int b = 20;
printf("ret:%d\n", a > b ? a : b);
//思考一个问题,(a > b ? a : b) 三目运算表达式返回的是什么?
cout << "b:" << b << endl;
//返回的是左值,变量的引用
(a > b ? a : b) = 100;//返回的是左值,变量的引用
cout << "b:" << b << endl;
[左值和右值概念]
在c++中可以放在赋值操作符左边的是左值,可以放到赋值操作符右面的是右值。
有些变量即可以当左值,也可以当右值。
左值为Lvalue,L代表Location,表示内存可以寻址,可以赋值。
右值为Rvalue,R代表Read,就是可以知道它的值。
比如:int temp = 10; temp在内存中有地址,10没有,但是可以Read到它的值。
6.C/C++中的const
const单词字面意思为常数,不变的。它是c/c++中的一个关键字,是一个限定符,它用来限定一个变量不允许改变,它将一个对象转换成一个常量。
const int a = 10;
A = 100; //编译错误,const是一个常量,不可修改
6.1 C/C++中const的区别
常量的引进是在c++早期版本中,当时标准C规范正在制定。那时,尽管C委员会决定在C中引入const,但是,他们c中的const理解为”一个不能改变的普通变量”,也就是认为const应该是一个只读变量,既然是变量那么就会给const分配内存,并且在c中const是一个全局只读变量,c语言中const修饰的只读变量是外部连接的。
如果这么写:
const int arrSize = 10;
int arr[arrSize];
看似是一件合理的编码,但是这将得出一个错误。 因为arrSize占用某块内存,所以C编译器不知道它在编译时的值是多少?
C++中的const
在c++中,一个const不必创建内存空间,而在c中,一个const总是需要一块内存空间。在c++中,是否为const常量分配内存空间依赖于如何使用。一般说来,如果一个const仅仅用来把一个名字用一个值代替(就像使用#define一样),那么该存储局空间就不必创建。
如果存储空间没有分配内存的话,在进行完数据类型检查后,为了代码更加有效,值也许会折叠到代码中。
不过,取一个const地址, 或者把它定义为extern,则会为该const创建内存空间。
在c++中,出现在所有函数之外的const作用于整个文件(也就是说它在该文件外不可见),默认为内部连接,c++中其他的标识符一般默认为外部连接。
7.引用
- 变量名实质上是一段连续内存空间的别名,是一个标号(门牌号)
- 程序中通过变量来申请并命名内存空间
- 通过变量的名字可以使用存储空间
对一段连续的内存空间只能取一个别名吗? c++中新增了引用的概念,引用可以作为一个已定义变量的别名。 |
基本语法:
Type& ref = val; |
注意事项:
- &在此不是求地址运算,而是起标识作用。
- 类型标识符是指目标变量的类型
- 必须在声明引用变量时进行初始化。
- 引用初始化之后不能改变。
- 不能有NULL引用。必须确保引用是和一块合法的存储单元关联。
- 可以建立对数组的引用。
void test01(){
int a = 10;
//给变量a取一个别名b
int& b = a;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "------------" << endl;
//操作b就相当于操作a本身
b = 100;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "------------" << endl;
//一个变量可以有n个别名
int& c = a;
c = 200;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "c:" << c << endl;
cout << "------------" << endl;
//a,b,c的地址都是相同的
cout << "a:" << &a << endl;
cout << "b:" << &b << endl;
cout << "c:" << &c << endl;
}
//2. 使用引用注意事项
void test02(){
//1) 引用必须初始化
//int& ref; //报错:必须初始化引用
//2) 引用一旦初始化,不能改变引用
int a = 10;
int b = 20;
int& ref = a;
ref = b; //不能改变引用
//3) 不能对数组建立引用
int arr[10];
//int& ref3[10] = arr;
}
typedef int ArrRef[10];
int arr[10];
ArrRef& aRef = arr;
for (int i = 0; i < 10;i ++){
aRef[i] = i+1;
}
for (int i = 0; i < 10;i++){
cout << arr[i] << " ";
}
cout << endl;
//2. 建立数组引用方法二
int(&f)[10] = arr;
for (int i = 0; i < 10; i++){
f[i] = i+10;
}
for (int i = 0; i < 10; i++){
cout << arr[i] << " ";
}
cout << endl;
//值传递
void ValueSwap(int m,int n){
int temp = m;
m = n;
n = temp;
}
//地址传递
void PointerSwap(int* m,int* n){
int temp = *m;
*m = *n;
*n = temp;
}
//引用传递
void ReferenceSwap(int& m,int& n){
int temp = m;
m = n;
n = temp;
}
void test(){
int a = 10;
int b = 20;
//值传递
ValueSwap(a, b);
cout << "a:" << a << " b:" << b << endl;
//地址传递
PointerSwap(&a, &b);
cout << "a:" << a << " b:" << b << endl;
//引用传递
ReferenceSwap(a, b);
cout << "a:" << a << " b:" << b << endl;
}
通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单:
- 函数调用时传递的实参不必加“&”符
- 在被调函数中不必在参数前加“*”符
引用作为其它变量的别名而存在,因此在一些场合可以代替指针。C++主张用引用传递取代地址传递的方式,因为引用语法容易且不易出错。
//返回局部变量引用
int& TestFun01(){
int a = 10; //局部变量
return a;
}
//返回静态变量引用
int& TestFunc02(){
static int a = 20;
cout << "static int a : " << a << endl;
return a;
}
int main(){
//不能返回局部变量的引用
int& ret01 = TestFun01();
//如果函数做左值,那么必须返回引用
TestFunc02();
TestFunc02() = 100;
TestFunc02();
return EXIT_SUCCESS;
}
- 不能返回局部变量的引用。
- 函数当左值,必须返回引用。
7.1 引用的本质
引用的本质在c++内部实现是一个指针常量.
Type& ref = val; // Type* const ref = &val; |
c++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同,只是这个过程是编译器内部实现,用户不可见。
//发现是引用,转换为 int* const ref = &a;
void testFunc(int& ref){
ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
int a = 10;
int& aRef = a; //自动转换为 int* const aRef = &a;这也能说明引用为什么必须初始化
aRef = 20; //内部发现aRef是引用,自动帮我们转换为: *aRef = 20;
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
testFunc(a);
return EXIT_SUCCESS;
}
7.2 指针引用
在c语言中如果想改变一个指针的指向而不是它所指向的内容,函数声明可能这样:
void fun(int**); |
给指针变量取一个别名。
Type* pointer = NULL;
Type*& = pointer;
Type* pointer = NULL; Type*& = pointer;
例:
struct Teacher{
int mAge;
};
//指针间接修改teacher的年龄
void AllocateAndInitByPointer(Teacher** teacher){
*teacher = (Teacher*)malloc(sizeof(Teacher));
(*teacher)->mAge = 200;
}
//引用修改teacher年龄
void AllocateAndInitByReference(Teacher*& teacher){
teacher->mAge = 300;
}
void test(){
//创建Teacher
Teacher* teacher = NULL;
//指针间接赋值
AllocateAndInitByPointer(&teacher);
cout << "AllocateAndInitByPointer:" << teacher->mAge << endl;
//引用赋值,将teacher本身传到ChangeAgeByReference函数中
AllocateAndInitByReference(teacher);
cout << "AllocateAndInitByReference:" << teacher->mAge << endl;
free(teacher);
}
对于c++中的定义那个,语法清晰多了。函数参数变成指针的引用,用不着取得指针的地址
7.3常量引用
常量引用的定义格式:
const Type& ref = val; |
常量引用注意:
- 字面量不能赋给引用,但是可以赋给const引用
- const修饰的引用,不能修改。
void test01(){
int a = 100;
const int& aRef = a; //此时aRef就是a
//aRef = 200; 不能通过aRef的值
a = 100; //OK
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
}
void test02(){
//不能把一个字面量赋给引用
//int& ref = 100;
//但是可以把一个字面量赋给常引用
const int& ref = 100; //int temp = 200; const int& ret = temp;
}
[const引用使用场景]
常量引用主要用在函数的形参,尤其是类的拷贝/复制构造函数。
将函数的形参定义为常量引用的好处:
- 引用不产生新的变量,减少形参与实参传递时的开销。
- 由于引用可能导致实参随形参改变而改变,将其定义为常量引用可以消除这种副作用。
如果希望实参随着形参的改变而改变,那么使用一般的引用,如果不希望实参随着形参改变,那么使用常引用。
8.函数重载
实现函数重载的条件:
- 同一个作用域
- 参数个数不同
- 参数类型不同
- 参数顺序不同
//1. 函数重载条件
namespace A{
void MyFunc(){ cout << "无参数!" << endl; }
void MyFunc(int a){ cout << "a: " << a << endl; }
void MyFunc(string b){ cout << "b: " << b << endl; }
void MyFunc(int a, string b){ cout << "a: " << a << " b:" << b << endl;}
void MyFunc(string b, int a){cout << "a: " << a << " b:" << b << endl;}
}
//2.返回值不作为函数重载依据
namespace B{
void MyFunc(string b, int a){}
//int MyFunc(string b, int a){} //无法重载仅按返回值区分的函数
}
注意: 函数重载和默认参数一起使用,需要额外注意二义性问题的产生。
void MyFunc(string b){
cout << "b: " << b << endl;
}
//函数重载碰上默认参数
void MyFunc(string b, int a = 10){
cout << "a: " << a << " b:" << b << endl;
}
int main(){
MyFunc("hello"); //这时,两个函数都能匹配调用,产生二义性
return 0;
}
函数返回值不作为重载条件?
当编译器能从上下文中确定唯一的函数的时,如int ret = func(),这个当然是没有问题的。然而,我们在编写程序过程中可以忽略他的返回值。那么这个时候,一个函数为
void func(int x);另一个为int func(int x); 当我们直接调用func(10),这个时候编译器就不确定调用那个函数。所以在c++中禁止使用返回值作为重载的条件。
9.extern “C”
extern "C"的主要作用就是为了实现c++代码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而不是按c++的方式。
MyModule.h
#ifndef MYMODULE_H
#define MYMODULE_H
#include<stdio.h>
#if __cplusplus
extern "C"{
#endif
void func1();
int func2(int a,int b);
#if __cplusplus
}
#endif
#endif
TestExternC.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#if 0
#ifdef __cplusplus
extern "C" {
#if 0
void func1();
int func2(int a, int b);
#else
#include"MyModule.h"
#endif
}
#endif
#else
extern "C" void func1();
extern "C" int func2(int a, int b);
#endif
int main(){
func1();
cout << func2(10, 20) << endl;
return EXIT_SUCCESS;
}
10.C和C++中struct区别
- c语言struct只有变量
- c++语言struct 既有变量,也有函数
[struct和class的区别?]
class默认访问权限为private,struct默认访问权限为public.
- 封装
- 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
- 对变量和函数进行访问控制
- 访问权限
- 在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
- 在类的外部(作用域范围外),访问权限才有意义:public,private,protected
- 在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时, private和protected是同等级的,外部不允许访问
10.1将成员变量设置为private
- 可赋予客户端访问数据的一致性。
如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
- 可细微划分访问控制。
- 使用成员函数可使得我们对变量的控制处理更加精细。如果我们让所有的成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。
class AccessLevels{ public: //对只读属性进行只读访问 int getReadOnly(){ return readOnly; } //对读写属性进行读写访问 void setReadWrite(int val){ readWrite = val; } int getReadWrite(){ return readWrite; } //对只写属性进行只写访问 void setWriteOnly(int val){ writeOnly = val; } private: int readOnly; //对外只读访问 int noAccess; //外部不可访问 int readWrite; //读写访问 int writeOnly; //只写访问 };
例子:
设计立方体类
设计立方体类(Cube),求出立方体的面积( 2*a*b + 2*a*c + 2*b*c )和体积( a * b * c),分别用全局函数和成员函数判断两个立方体是否相等。
//立方体类
class Cub{
public:
void setL(int l){ mL = l; }
void setW(int w){ mW = w; }
void setH(int h){ mH = h; }
int getL(){ return mL; }
int getW(){ return mW; }
int getH(){ return mH; }
//立方体面积
int caculateS(){ return (mL*mW + mL*mH + mW*mH) * 2; }
//立方体体积
int caculateV(){ return mL * mW * mH; }
//成员方法
bool CubCompare(Cub& c){
if (getL() == c.getL() && getW() == c.getW() && getH() == c.getH()){
return true;
}
return false;
}
private:
int mL; //长
int mW; //宽
int mH; //高
};
//比较两个立方体是否相等
bool CubCompare(Cub& c1, Cub& c2){
if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()){
return true;
}
return false;
}
void test(){
Cub c1, c2;
c1.setL(10);
c1.setW(20);
c1.setH(30);
c2.setL(20);
c2.setW(20);
c2.setH(30);
cout << "c1面积:" << c1.caculateS() << " 体积:" << c1.caculateV() << endl;
cout << "c2面积:" << c2.caculateS() << " 体积:" << c2.caculateV() << endl;
//比较两个立方体是否相等
if (CubCompare(c1, c2)){
cout << "c1和c2相等!" << endl;
}
else{
cout << "c1和c2不相等!" << endl;
}
if (c1.CubCompare(c2)){
cout << "c1和c2相等!" << endl;
}
else{
cout << "c1和c2不相等!" << endl;
}
}
例2
设计一个圆形类(AdvCircle),和一个点类(Point),计算点和圆的关系。
假如圆心坐标为x0, y0, 半径为r,点的坐标为x1, y1:
- 点在圆上:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) == r*r
- 点在圆内:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) < r*r
- 点在圆外:(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) > r*r
//点类
class Point{
public:
void setX(int x){ mX = x; }
void setY(int y){ mY = y; }
int getX(){ return mX; }
int getY(){ return mY; }
private:
int mX;
int mY;
};
//圆类
class Circle{
public:
void setP(int x,int y){
mP.setX(x);
mP.setY(y);
}
void setR(int r){ mR = r; }
Point& getP(){ return mP; }
int getR(){ return mR; }
//判断点和圆的关系
void IsPointInCircle(Point& point){
int distance = (point.getX() - mP.getX()) * (point.getX() - mP.getX()) + (point.getY() - mP.getY()) * (point.getY() - mP.getY());
int radius = mR * mR;
if (distance < radius){
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆内!" << endl;
}
else if (distance > radius){
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆外!" << endl;
}
else{
cout << "Point(" << point.getX() << "," << point.getY() << ")在圆上!" << endl;
}
}
private:
Point mP; //圆心
int mR; //半径
};
void test(){
//实例化圆对象
Circle circle;
circle.setP(20, 20);
circle.setR(5);
//实例化点对象
Point point;
point.setX(25);
point.setY(20);
circle.IsPointInCircle(point);
}
11.0对象的构造和析构
11.1构造函数和析构函数
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:
- 构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数。
- ClassName(){}
析构函数语法:
- 析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。
- ~ClassName(){}
例
class Person{
public:
Person(){
cout << "构造函数调用!" << endl;
pName = (char*)malloc(sizeof("John"));
strcpy(pName, "John");
mTall = 150;
mMoney = 100;
}
~Person(){
cout << "析构函数调用!" << endl;
if (pName != NULL){
free(pName);
pName = NULL;
}
}
public:
char* pName;
int mTall;
int mMoney;
};
void test(){
Person person;
cout << person.pName << person.mTall << person.mMoney << endl;
}
11.2 构造函数的分类及调用
- 按参数类型:分为无参构造函数和有参构造函数
- 按类型分类:普通构造函数和拷贝构造函数(复制构造函数)
例
class Person{
public:
Person(){
cout << "no param constructor!" << endl;
mAge = 0;
}
//有参构造函数
Person(int age){
cout << "1 param constructor!" << endl;
mAge = age;
}
//拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象
Person(const Person& person){
cout << "copy constructor!" << endl;
mAge = person.mAge;
}
//打印年龄
void PrintPerson(){
cout << "Age:" << mAge << endl;
}
private:
int mAge;
};
//1. 无参构造调用方式
void test01(){
//调用无参构造函数
Person person1;
person1.PrintPerson();
//无参构造函数错误调用方式
//Person person2();
//person2.PrintPerson();
}
//2. 调用有参构造函数
void test02(){
//第一种 括号法,最常用
Person person01(100);
person01.PrintPerson();
//调用拷贝构造函数
Person person02(person01);
person02.PrintPerson();
//第二种 匿名对象(显示调用构造函数)
Person(200); //匿名对象,没有名字的对象
Person person03 = Person(300);
person03.PrintPerson();
//注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型
Person person06(Person(400)); //等价于 Person person06 = Person(400);
person06.PrintPerson();
//第三种 =号法 隐式转换
Person person04 = 100; //Person person04 = Person(100)
person04.PrintPerson();
//调用拷贝构造
Person person05 = person04; //Person person05 = Person(person04)
person05.PrintPerson();
}
b为A的实例化对象,A a = A(b) 和 A(b)的区别?
当A(b) 有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接的时候,编译器认为你A(b) 等价于 A b.
注意:不能调用拷贝构造函数去初始化匿名对象,也就是说以下代码不正确:
class Teacher{
public:
Teacher(){
cout << "默认构造函数!" << endl;
}
Teacher(const Teacher& teacher){
cout << "拷贝构造函数!" << endl;
}
public:
int mAge;
};
void test(){
Teacher t1;
//error C2086:“Teacher t1”: 重定义
Teacher(t1); //此时等价于 Teacher t1;
}
11.3拷贝构造函数的调用时机
- 对象以值传递的方式传给函数参数
- 函数局部对象以值传递的方式从函数返回(vs debug模式下调用一次拷贝构造,qt不调用任何构造)
- 用一个对象初始化另一个对象
class Person{
public:
Person(){
cout << "no param contructor!" << endl;
mAge = 10;
}
Person(int age){
cout << "param constructor!" << endl;
mAge = age;
}
Person(const Person& person){
cout << "copy constructor!" << endl;
mAge = person.mAge;
}
~Person(){
cout << "destructor!" << endl;
}
public:
int mAge;
};
//1. 旧对象初始化新对象
void test01(){
Person p(10);
Person p1(p);
Person p2 = Person(p);
Person p3 = p; // 相当于Person p2 = Person(p);
}
//2. 传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造
void doBussiness(Person p){}
void test02(){
Person p(10);
doBussiness(p);
}
//3. 函数返回局部对象
Person MyBusiness(){
Person p(10);
cout << "局部p:" << (int*)&p << endl;
return p;
}
void test03(){
//vs release、qt下没有调用拷贝构造函数
//vs debug下调用一次拷贝构造函数
Person p = MyBusiness();
cout << "局部p:" << (int*)&p << endl;
}
[Test03结果说明:]
编译器存在一种对返回值的优化技术,RVO(Return Value Optimization).在vs debug模式下并没有进行这种优化,所以函数MyBusiness中创建p对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时Person对象返回,然后调用p的析构函数。
我们从常理来分析的话,这个匿名对象和这个局部的p对象是相同的两个对象,那么如果能直接返回p对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。
所以在这里,编译器偷偷帮我们做了一层优化:
当我们这样去调用: Person p = MyBusiness();
编译器偷偷将我们的代码更改为:
void MyBussiness(Person& _result){
_result.X:X(); //调用Person默认拷贝构造函数
//.....对_result进行处理
return;
}
int main(){
Person p; //这里只分配空间,不初始化
MyBussiness(p);
}
11.4构造函数调用规则
- 默认情况下,c++编译器至少为我们写的类增加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对类中非静态成员属性简单值拷贝
- 如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
- 如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造
11.5深拷贝和浅拷贝
11.5.1浅拷贝
同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝.
一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
11.5.2 深拷贝
当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
class Person{
public:
Person(char* name,int age){
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName,name);
mAge = age;
}
//增加拷贝构造函数
Person(const Person& person){
pName = (char*)malloc(strlen(person.pName) + 1);
strcpy(pName, person.pName);
mAge = person.mAge;
}
~Person(){
if (pName != NULL){
free(pName);
}
}
private:
char* pName;
int mAge;
};
void test(){
Person p1("Edward",30);
//用对象p1初始化对象p2,调用c++提供的默认拷贝构造函数
Person p2 = p1;
}
11.6多个对象构造和析构
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用:
class Person{
public:
#if 0
//传统方式初始化
Person(int a,int b,int c){
mA = a;
mB = b;
mC = c;
}
#endif
//初始化列表方式初始化
Person(int a, int b, int c):mA(a),mB(b),mC(c){}
void PrintPerson(){
cout << "mA:" << mA << endl;
cout << "mB:" << mB << endl;
cout << "mC:" << mC << endl;
}
private:
int mA;
int mB;
int mC;
};
注意:初始化成员列表(参数列表)只能在构造函数使用。
类对象作为成员
在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。
C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,c++编译器必须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?
那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。
解决办法非常简单:对于子类调用构造函数,c++为此提供了专门的语法,即构造函数初始化列表。
当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。
析构函数和构造函数调用顺序相反,先构造,后析构。
//汽车类
class Car{
public:
Car(){
cout << "Car 默认构造函数!" << endl;
mName = "大众汽车";
}
Car(string name){
cout << "Car 带参数构造函数!" << endl;
mName = name;
}
~Car(){
cout << "Car 析构函数!" << endl;
}
public:
string mName;
};
//拖拉机
class Tractor{
public:
Tractor(){
cout << "Tractor 默认构造函数!" << endl;
mName = "爬土坡专用拖拉机";
}
Tractor(string name){
cout << "Tractor 带参数构造函数!" << endl;
mName = name;
}
~Tractor(){
cout << "Tractor 析构函数!" << endl;
}
public:
string mName;
};
//人类
class Person{
public:
#if 1
//类mCar不存在合适的构造函数
Person(string name){
mName = name;
}
#else
//初始化列表可以指定调用构造函数
Person(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){
cout << "Person 构造函数!" << endl;
}
#endif
void GoWorkByCar(){
cout << mName << "开着" << mCar.mName << "去上班!" << endl;
}
void GoWorkByTractor(){
cout << mName << "开着" << mTractor.mName << "去上班!" << endl;
}
~Person(){
cout << "Person 析构函数!" << endl;
}
private:
string mName;
Car mCar;
Tractor mTractor;
};
void test(){
//Person person("宝马", "东风拖拉机", "赵四");
Person person("刘能");
person.GoWorkByCar();
person.GoWorkByTractor();
}
11.7 explicit关键字
c++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。
[explicit注意]
- explicit用于修饰构造函数,防止隐式转化。
- 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
class MyString{
public:
explicit MyString(int n){
cout << "MyString(int n)!" << endl;
}
MyString(const char* str){
cout << "MyString(const char* str)" << endl;
}
};
int main(){
//给字符串赋值?还是初始化?
//MyString str1 = 1;
MyString str2(10);
//寓意非常明确,给字符串赋值
MyString str3 = "abcd";
MyString str4("abcd");
return EXIT_SUCCESS;
}
11.8动态对象创建
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
所以动态的意思意味着不确定性。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。
然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
对象创建
当创建一个c++对象时会发生两件事:
- 为对象分配内存
- 调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。
动态分配内存方法
为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
class Person{
public:
Person(){
mAge = 20;
pName = (char*)malloc(strlen("john")+1);
strcpy(pName, "john");
}
void Init(){
mAge = 20;
pName = (char*)malloc(strlen("john")+1);
strcpy(pName, "john");
}
void Clean(){
if (pName != NULL){
free(pName);
}
}
public:
int mAge;
char* pName;
};
int main(){
//分配内存
Person* person = (Person*)malloc(sizeof(Person));
if(person == NULL){
return 0;
}
//调用初始化函数
person->Init();
//清理对象
person->Clean();
//释放person对象
free(person);
return EXIT_SUCCESS;
}
- 程序员必须确定对象的长度。
- malloc返回一个void*指针,c++不允许将void*赋值给其他任何指针,必须强转。
- malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
- 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。
c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和 delete.
new operator
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
Person* person = new Person;
相当于:
Person* person = (Person*)malloc(sizeof(Person));
if(person == NULL){
return 0;
}
person->Init(); 构造函数
New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。
现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
delete operator
new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
delete只适用于由new创建的对象。
如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。
class Person{
public:
Person(){
cout << "无参构造函数!" << endl;
pName = (char*)malloc(strlen("undefined") + 1);
strcpy(pName, "undefined");
mAge = 0;
}
Person(char* name, int age){
cout << "有参构造函数!" << endl;
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName, name);
mAge = age;
}
void ShowPerson(){
cout << "Name:" << pName << " Age:" << mAge << endl;
}
~Person(){
cout << "析构函数!" << endl;
if (pName != NULL){
delete pName;
pName = NULL;
}
}
public:
char* pName;
int mAge;
};
void test(){
Person* person1 = new Person;
Person* person2 = new Person("John",33);
person1->ShowPerson();
person2->ShowPerson();
delete person1;
delete person2;
}
用于数组的new和delete
用于数组的new和delete
使用new和delete在堆上创建数组非常容易。
//创建字符数组
char* pStr = new char[100];
//创建整型数组
int* pArr1 = new int[100];
//创建整型数组并初始化
int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。
class Person{
public:
Person(){
pName = (char*)malloc(strlen("undefined") + 1);
strcpy(pName, "undefined");
mAge = 0;
}
Person(char* name, int age){
pName = (char*)malloc(sizeof(name));
strcpy(pName, name);
mAge = age;
}
~Person(){
if (pName != NULL){
delete pName;
}
}
public:
char* pName;
int mAge;
};
void test(){
//栈聚合初始化
Person person[] = { Person("john", 20), Person("Smith", 22) };
cout << person[1].pName << endl;
//创建堆上对象数组必须提供构造函数
Person* workers = new Person[20];
}
delete void*可能会出错
如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少。
class Person{
public:
Person(char* name, int age){
pName = (char*)malloc(sizeof(name));
strcpy(pName,name);
mAge = age;
}
~Person(){
if (pName != NULL){
delete pName;
}
}
public:
char* pName;
int mAge;
};
void test(){
void* person = new Person("john",20);
delete person;
}
问题:malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?
11.9静态成员
在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字static声明为静态的,称为静态成员。
不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。
静态成员变量
在一个类中,若将一个成员变量声明为static,这种成员称为静态成员变量。与一般的数据成员不同,无论建立了多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,所有对象共享。
静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
- 静态成员变量必须在类中声明,在类外定义。
- 静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。
- 静态数据成员可以通过类名或者对象名来引用。
class Person{
public:
//类的静态成员属性
static int sNum;
private:
static int sOther;
};
//类外初始化,初始化时不加static
int Person::sNum = 0;
int Person::sOther = 0;
int main(){
//1. 通过类名直接访问
Person::sNum = 100;
cout << "Person::sNum:" << Person::sNum << endl;
//2. 通过对象访问
Person p1, p2;
p1.sNum = 200;
cout << "p1.sNum:" << p1.sNum << endl;
cout << "p2.sNum:" << p2.sNum << endl;
//3. 静态成员也有访问权限,类外不能访问私有成员
//cout << "Person::sOther:" << Person::sOther << endl;
Person p3;
//cout << "p3.sOther:" << p3.sOther << endl;
system("pause");
return EXIT_SUCCESS;
}
静态成员函数
在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。
静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
- 静态成员函数只能访问静态变量,不能访问普通成员变量
- 静态成员函数的使用和静态成员变量一样
- 静态成员函数也有访问权限
- 普通成员函数可访问静态成员变量、也可以访问非经常成员变量
class Person{
public:
//普通成员函数可以访问static和non-static成员属性
void changeParam1(int param){
mParam = param;
sNum = param;
}
//静态成员函数只能访问static成员属性
static void changeParam2(int param){
//mParam = param; //无法访问
sNum = param;
}
private:
static void changeParam3(int param){
//mParam = param; //无法访问
sNum = param;
}
public:
int mParam;
static int sNum;
};
//静态成员属性类外初始化
int Person::sNum = 0;
int main(){
//1. 类名直接调用
Person::changeParam2(100);
//2. 通过对象调用
Person p;
p.changeParam2(200);
//3. 静态成员函数也有访问权限
//Person::changeParam3(100); //类外无法访问私有静态成员函数
//Person p1;
//p1.changeParam3(200);
return EXIT_SUCCESS;
}
const静态成员属性
如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static const 修饰。定义静态const数据成员时,最好在类内部初始化。
class Person{
public:
//static const int mShare = 10;
const static int mShare = 10; //只读区,不可修改
};
int main(){
cout << Person::mShare << endl;
//Person::mShare = 20;
return EXIT_SUCCESS;
}
静态成员实现单例模式
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
用单例模式,模拟公司员工使用打印机场景,打印机可以打印员工要输出的内容,并且可以累积打印机使用次数。 |
class Printer{
public:
static Printer* getInstance(){ return pPrinter;}
void PrintText(string text){
cout << "打印内容:" << text << endl;
cout << "已打印次数:" << mTimes << endl;
cout << "--------------" << endl;
mTimes++;
}
private:
Printer(){ mTimes = 0; }
Printer(const Printer&){}
private:
static Printer* pPrinter;
int mTimes;
};
Printer* Printer::pPrinter = new Printer;
void test(){
Printer* printer = Printer::getInstance();
printer->PrintText("离职报告!");
printer->PrintText("入职合同!");
printer->PrintText("提交代码!");
}
12.0C++面向对象模型初探
12.1 成员变量和函数的存储
在c语言中,“分开来声明的,也就是说,语言本身并没有支持“数据”和“函数”之间的关联性我们把这种程序方法称为“程序性的”,由一组“分布在各个以功能为导航的函数中”的算法驱动,它们处理的是共同的外部数据。
c++实现了“封装”,那么数据(成员属性)和操作(成员函数)是什么样的呢?
“数据”和“处理数据的操作(函数)”是分开存储的。
- c++中的非静态数据成员直接内含在类对象中,就像c struct一样。
- 成员函数(member function)虽然内含在class声明之内,却不出现在对象中。
- 每一个非内联成员函数(non-inline member function)只会诞生一份函数实例.
class MyClass01{
public:
int mA;
};
class MyClass02{
public:
int mA;
static int sB;
};
class MyClass03{
public:
void printMyClass(){
cout << "hello world!" << endl;
}
public:
int mA;
static int sB;
};
class MyClass04{
public:
void printMyClass(){
cout << "hello world!" << endl;
}
static void ShowMyClass(){
cout << "hello world!" << endl;
}
public:
int mA;
static int sB;
};
int main(){
MyClass01 mclass01;
MyClass02 mclass02;
MyClass03 mclass03;
MyClass04 mclass04;
cout << "MyClass01:" << sizeof(mclass01) << endl; //4
//静态数据成员并不保存在类对象中
cout << "MyClass02:" << sizeof(mclass02) << endl; //4
//非静态成员函数不保存在类对象中
cout << "MyClass03:" << sizeof(mclass03) << endl; //4
//静态成员函数也不保存在类对象中
cout << "MyClass04:" << sizeof(mclass04) << endl; //4
return EXIT_SUCCESS;
}
通过上面的案例,我们可以的得出:C++类对象中的变量和函数是分开存储。
12.2 this指针工作原理
通过上例我们知道,c++的数据和操作也是分开存储,并且每一个非内联成员函数(non-inline member function)只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象。
c++规定,this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址,也就是说虽然我们没有写上this指针,编译器在编译的时候也是会加上的。因此this也称为“指向本对象的指针”,this指针并不是对象的一部分,不会影响sizeof(对象)的结果。
this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,在外部看来,每一个对象都拥有自己的函数成员。一般情况下,并不写this,而是让系统进行默认设置。
this指针永远指向当前对象。
成员函数通过this指针即可知道操作的是那个对象的数据。This指针是一种隐含指针,它隐含于每个类的非静态成员函数中。This指针无需定义,直接使用即可。
注意:静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量。
c++编译器对普通成员函数的内部处理 |
12.3 this指针的使用
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this.
class Person{
public:
//1. 当形参名和成员变量名一样时,this指针可用来区分
Person(string name,int age){
//name = name;
//age = age; //输出错误
this->name = name;
this->age = age;
}
//2. 返回对象本身的引用
//重载赋值操作符
//其实也是两个参数,其中隐藏了一个this指针
Person PersonPlusPerson(Person& person){
string newname = this->name + person.name;
int newage = this->age + person.age;
Person newperson(newname, newage);
return newperson;
}
void ShowPerson(){
cout << "Name:" << name << " Age:" << age << endl;
}
public:
string name;
int age;
};
//3. 成员函数和全局函数(Perosn对象相加)
Person PersonPlusPerson(Person& p1,Person& p2){
string newname = p1.name + p2.name;
int newage = p1.age + p2.age;
Person newperson(newname,newage);
return newperson;
}
int main(){
Person person("John",100);
person.ShowPerson();
cout << "---------" << endl;
Person person1("John",20);
Person person2("001", 10);
//1.全局函数实现两个对象相加
Person person3 = PersonPlusPerson(person1, person2);
person1.ShowPerson();
person2.ShowPerson();
person3.ShowPerson();
//2. 成员函数实现两个对象相加
Person person4 = person1.PersonPlusPerson(person2);
person4.ShowPerson();
system("pause");
return EXIT_SUCCESS;
}
const修饰成员函数
- 用const修饰的成员函数时,const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量,
- 当成员变量类型符前用mutable修饰时例外。
//const修饰成员函数
class Person{
public:
Person(){
this->mAge = 0;
this->mID = 0;
}
//在函数括号后面加上const,修饰成员变量不可修改,除了mutable变量
void sonmeOperate() const{
//this->mAge = 200; //mAge不可修改
this->mID = 10;
}
void ShowPerson(){
cout << "ID:" << mID << " mAge:" << mAge << endl;
}
private:
int mAge;
mutable int mID;
};
int main(){
Person person;
person.sonmeOperate();
person.ShowPerson();
system("pause");
return EXIT_SUCCESS;
}
const修饰对象(常对象)
- 常对象只能调用const的成员函数
- 常对象可访问 const 或非 const 数据成员,不能修改,除非成员用mutable修饰
class Person{
public:
Person(){
this->mAge = 0;
this->mID = 0;
}
void ChangePerson() const{
mAge = 100;
mID = 100;
}
void ShowPerson(){
this->mAge = 1000;
cout << "ID:" << this->mID << " Age:" << this->mAge << endl;
}
public:
int mAge;
mutable int mID;
};
void test(){
const Person person;
//1. 可访问数据成员
cout << "Age:" << person.mAge << endl;
//person.mAge = 300; //不可修改
person.mID = 1001; //但是可以修改mutable修饰的成员变量
//2. 只能访问const修饰的函数
//person.ShowPerson();
person.ChangePerson();
}
13.0 友元
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,怎么办?
解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。这一点从现实生活中也可以很好的理解:
比如你的家,有客厅,有你的卧室,那么你的客厅是Public的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的闺蜜好基友进去。
程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
友元语法
- friend关键字只出现在声明处
- 其他类、类成员函数、全局函数都可声明为友元
- 友元函数不是类的成员,不带this指针
- 友元函数可访问对象任意成员属性,包括私有属性
class Building;
//友元类
class MyFriend{
public:
//友元成员函数
void LookAtBedRoom(Building& building);
void PlayInBedRoom(Building& building);
};
class Building{
//全局函数做友元函数
friend void CleanBedRoom(Building& building);
#if 0
//成员函数做友元函数
friend void MyFriend::LookAtBedRoom(Building& building);
friend void MyFriend::PlayInBedRoom(Building& building);
#else
//友元类
friend class MyFriend;
#endif
public:
Building();
public:
string mSittingRoom;
private:
string mBedroom;
};
void MyFriend::LookAtBedRoom(Building& building){
cout << "我的朋友参观" << building.mBedroom << endl;
}
void MyFriend::PlayInBedRoom(Building& building){
cout << "我的朋友玩耍在" << building.mBedroom << endl;
}
//友元全局函数
void CleanBedRoom(Building& building){
cout << "友元全局函数访问" << building.mBedroom << endl;
}
Building::Building(){
this->mSittingRoom = "客厅";
this->mBedroom = "卧室";
}
int main(){
Building building;
MyFriend myfriend;
CleanBedRoom(building);
myfriend.LookAtBedRoom(building);
myfriend.PlayInBedRoom(building);
system("pause");
return EXIT_SUCCESS;
}
[友元类注意]
- 友元关系不能被继承。
- 友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友。
友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。
思考: c++是纯面向对象的吗?
如果一个类被声明为friend,意味着它不是这个类的成员函数,却可以修改这个类的私有成员,而且必须列在类的定义中,因此他是一个特权函数。c++不是完全的面向对象语言,而只是一个混合产品。增加friend关键字只是用来解决一些实际问题,这也说明这种语言是不纯的。毕竟c++设计的目的是为了实用性,而不是追求理想的抽象。
--- Thinking in C++
例:
请编写电视机类,电视机有开机和关机状态,有音量,有频道,提供音量操作的方法,频道操作的方法。由于电视机只能逐一调整频道,不能指定频道,增加遥控类,遥控类除了拥有电视机已有的功能,再增加根据输入调台功能。
class Remote;
class Television{
friend class Remote;
public:
enum{ On,Off }; //电视状态
enum{ minVol,maxVol = 100 }; //音量从0到100
enum{ minChannel = 1,maxChannel = 255 }; //频道从1到255
Television(){
mState = Off;
mVolume = minVol;
mChannel = minChannel;
}
//打开电视机
void OnOrOff(){
this->mState = (this->mState == On ? Off : On);
}
//调高音量
void VolumeUp(){
if (this->mVolume >= maxVol){
return;
}
this->mVolume++;
}
//调低音量
void VolumeDown(){
if (this->mVolume <= minVol){
return;
}
this->mVolume--;
}
//更换电视频道
void ChannelUp(){
if (this->mChannel >= maxChannel){
return;
}
this->mChannel++;
}
void ChannelDown(){
if (this->mChannel <= minChannel){
return;
}
this->mChannel--;
}
//展示当前电视状态信息
void ShowTeleState(){
cout << "开机状态:" << (mState == On ? "已开机" : "已关机") << endl;
if (mState == On){
cout << "当前音量:" << mVolume << endl;
cout << "当前频道:" << mChannel << endl;
}
cout << "-------------" << endl;
}
private:
int mState; //电视状态,开机,还是关机
int mVolume; //电视机音量
int mChannel; //电视频道
};
//电视机调台只能一个一个的调,遥控可以指定频道
//电视遥控器
class Remote{
public:
Remote(Television* television){
pTelevision = television;
}
public:
void OnOrOff(){
pTelevision->OnOrOff();
}
//调高音量
void VolumeUp(){
pTelevision->VolumeUp();
}
//调低音量
void VolumeDown(){
pTelevision->VolumeDown();
}
//更换电视频道
void ChannelUp(){
pTelevision->ChannelUp();
}
void ChannelDown(){
pTelevision->ChannelDown();
}
//设置频道 遥控新增功能
void SetChannel(int channel){
if (channel < Television::minChannel || channel > Television::maxChannel){
return;
}
pTelevision->mChannel = channel;
}
//显示电视当前信息
void ShowTeleState(){
pTelevision->ShowTeleState();
}
private:
Television* pTelevision;
};
//直接操作电视
void test01(){
Television television;
television.ShowTeleState();
television.OnOrOff(); //开机
television.VolumeUp(); //增加音量+1
television.VolumeUp(); //增加音量+1
television.VolumeUp(); //增加音量+1
television.VolumeUp(); //增加音量+1
television.ChannelUp(); //频道+1
television.ChannelUp(); //频道+1
television.ShowTeleState();
}
//通过遥控操作电视
void test02(){
//创建电视
Television television;
//创建遥控
Remote remote(&television);
remote.OnOrOff();
remote.ChannelUp();//频道+1
remote.ChannelUp();//频道+1
remote.ChannelUp();//频道+1
remote.VolumeUp();//音量+1
remote.VolumeUp();//音量+1
remote.VolumeUp();//音量+1
remote.VolumeUp();//音量+1
remote.ShowTeleState();
}
例2.数组类封装MyArray.h
#ifndef MYARRAY_H
#define MYARRAY_H
class MyArray{
public:
//无参构造函数,用户没有指定容量,则初始化为100
MyArray();
//有参构造函数,用户指定容量初始化
explicit MyArray(int capacity);
//用户操作接口
//根据位置添加元素
void SetData(int pos, int val);
//获得指定位置数据
int GetData(int pos);
//尾插法
void PushBack(int val);
//获得长度
int GetLength();
//析构函数,释放数组空间
~MyArray();
private:
int mCapacity; //数组一共可容纳多少个元素
int mSize; //当前有多少个元素
int* pAdress; //指向存储数据的空间
};
#endif
MyArray.cpp
#include"MyArray.h"
MyArray::MyArray(){
this->mCapacity = 100;
this->mSize = 0;
//在堆开辟空间
this->pAdress = new int[this->mCapacity];
}
//有参构造函数,用户指定容量初始化
MyArray::MyArray(int capacity){
this->mCapacity = capacity;
this->mSize = 0;
//在堆开辟空间
this->pAdress = new int[capacity];
}
//根据位置添加元素
void MyArray::SetData(int pos, int val){
if (pos < 0 || pos > mCapacity - 1){
return;
}
pAdress[pos] = val;
}
//获得指定位置数据
int MyArray::GetData(int pos){
return pAdress[pos];
}
//尾插法
void MyArray::PushBack(int val){
if (mSize >= mCapacity){
return;
}
this->pAdress[mSize] = val;
this->mSize++;
}
//获得长度
int MyArray::GetLength(){
return this->mSize;
}
//析构函数,释放数组空间
MyArray::~MyArray(){
if (this->pAdress != nullptr){
delete[] this->pAdress;
}
}
TestMyArray.cpp
#include"MyArray.h"
void test(){
//创建数组
MyArray myarray(50);
//数组中插入元素
for (int i = 0; i < 50; i++){
//尾插法
myarray.PushBack(i);
//myarray.SetData(i, i);
}
//打印数组中元素
for (int i = 0; i < myarray.GetLength(); i++){
cout << myarray.GetData(i) << " ";
}
cout << endl;
}
14 运算符重载
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
运算符重载(operator overloading)只是一种”语法上的方便”,也就是它只是另一种函数调用的方式。 |
在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
语法:
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。
- 运算符是一元(一个参数)的还是二元(两个参数);
- 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数)
14.1运算符重载碰上友元函数
友元函数是一个全局函数,和我们上例写的全局函数类似,只是友元函数可以访问某个类私有数据。
案例: 重载左移操作符(<<),使得cout可以输出对象。
class Person{
friend ostream& operator<<(ostream& os, Person& person);
public:
Person(int id,int age){
mID = id;
mAge = age;
}
private:
int mID;
int mAge;
};
ostream& operator<<(ostream& os, Person& person){
os << "ID:" << person.mID << " Age:" << person.mAge;
return os;
}
int main(){
Person person(1001, 30);
//cout << person; //cout.operator+(person)
cout << person << " | " << endl;
return EXIT_SUCCESS;
}
14.2可重载的运算符
几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能使用C中当前没有意义的运算符(例如用**求幂)不能改变运算符优先级,不能改变运算符的参数个数。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清寓语意。
14.3 自增自减(++/--)运算符重载
重载的++和--运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).
class Complex{
friend ostream& operator<<(ostream& os,Complex& complex){
os << "A:" << complex.mA << " B:" << complex.mB << endl;
return os;
}
public:
Complex(){
mA = 0;
mB = 0;
}
//重载前置++
Complex& operator++(){
mA++;
mB++;
return *this;
}
//重载后置++
Complex operator++(int){
Complex temp;
temp.mA = this->mA;
temp.mB = this->mB;
mA++;
mB++;
return temp;
}
//前置--
Complex& operator--(){
mA--;
mB--;
return *this;
}
//后置--
Complex operator--(int){
Complex temp;
temp.mA = mA;
temp.mB = mB;
mA--;
mB--;
return temp;
}
void ShowComplex(){
cout << "A:" << mA << " B:" << mB << endl;
}
private:
int mA;
int mB;
};
void test(){
Complex complex;
complex++;
cout << complex;
++complex;
cout << complex;
Complex ret = complex++;
cout << ret;
cout << complex;
cout << "------" << endl;
ret--;
--ret;
cout << "ret:" << ret;
complex--;
--complex;
cout << "complex:" << complex;
}
14.4指针运算符(*、->)重载
class Person{
public:
Person(int param){
this->mParam = param;
}
void PrintPerson(){
cout << "Param:" << mParam << endl;
}
private:
int mParam;
};
class SmartPointer{
public:
SmartPointer(Person* person){
this->pPerson = person;
}
//重载指针的->、*操作符
Person* operator->(){
return pPerson;
}
Person& operator*(){
return *pPerson;
}
~SmartPointer(){
if (pPerson != NULL){
delete pPerson;
}
}
public:
Person* pPerson;
};
void test01(){
//Person* person = new Person(100);
//如果忘记释放,那么就会造成内存泄漏
SmartPointer pointer(new Person(100));
pointer->PrintPerson();
}
14.5赋值(=)运算符重载
赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
class Person{
friend ostream& operator<<(ostream& os,const Person& person){
os << "ID:" << person.mID << " Age:" << person.mAge << endl;
return os;
}
public:
Person(int id,int age){
this->mID = id;
this->mAge = age;
}
//重载赋值运算符
Person& operator=(const Person& person){
this->mID = person.mID;
this->mAge = person.mAge;
return *this;
}
private:
int mID;
int mAge;
};
//1. =号混淆的地方
void test01(){
Person person1(10, 20);
Person person2 = person1; //调用拷贝构造
//如果一个对象还没有被创建,则必须初始化,也就是调用构造函数
//上述例子由于person2还没有初始化,所以会调用构造函数
//由于person2是从已有的person1来创建的,所以只有一个选择
//就是调用拷贝构造函数
person2 = person1; //调用operator=函数
//由于person2已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符
}
//2. 赋值重载案例
void test02(){
Person person1(20, 20);
Person person2(30, 30);
cout << "person1:" << person1;
cout << "person2:" << person2;
person2 = person1;
cout << "person2:" << person2;
}
//常见错误,当准备给两个相同对象赋值时,应该首先检查一下这个对象是否对自身赋值了
//对于本例来讲,无论如何执行这些赋值运算都是无害的,但如果对类的实现进行修改,那么将会出现差异;
//3. 类中指针
class Person2{
friend ostream& operator<<(ostream& os, const Person2& person){
os << "Name:" << person.pName << " ID:" << person.mID << " Age:" << person.mAge << endl;
return os;
}
public:
Person2(char* name,int id, int age){
this->pName = new char[strlen(name) + 1];
strcpy(this->pName, name);
this->mID = id;
this->mAge = age;
}
#if 1
//重载赋值运算符
Person2& operator=(const Person2& person){
//注意:由于当前对象已经创建完毕,那么就有可能pName指向堆内存
//这个时候如果直接赋值,会导致内存没有及时释放
if (this->pName != NULL){
delete[] this->pName;
}
this->pName = new char[strlen(person.pName) + 1];
strcpy(this->pName,person.pName);
this->mID = person.mID;
this->mAge = person.mAge;
return *this;
}
#endif
//析构函数
~Person2(){
if (this->pName != NULL){
delete[] this->pName;
}
}
private:
char* pName;
int mID;
int mAge;
};
void test03(){
Person2 person1("John",20, 20);
Person2 person2("Edward",30, 30);
cout << "person1:" << person1;
cout << "person2:" << person2;
person2 = person1;
cout << "person2:" << person2;
}
14.6等于和不等于(==、!=)运算符重载
class Complex{
public:
Complex(char* name,int id,int age){
this->pName = new char[strlen(name) + 1];
strcpy(this->pName, name);
this->mID = id;
this->mAge = age;
}
//重载==号操作符
bool operator==(const Complex& complex){
if (strcmp(this->pName,complex.pName) == 0 &&
this->mID == complex.mID &&
this->mAge == complex.mAge){
return true;
}
return false;
}
//重载!=操作符
bool operator!=(const Complex& complex){
if (strcmp(this->pName, complex.pName) != 0 ||
this->mID != complex.mID ||
this->mAge != complex.mAge){
return true;
}
return false;
}
~Complex(){
if (this->pName != NULL){
delete[] this->pName;
}
}
private:
char* pName;
int mID;
int mAge;
};
void test(){
Complex complex1("aaa", 10, 20);
Complex complex2("bbb", 10, 20);
if (complex1 == complex2){ cout << "相等!" << endl; }
if (complex1 != complex2){ cout << "不相等!" << endl; }
}
14.7函数调用符号()重载
class Complex{
public:
int Add(int x,int y){
return x + y;
}
int operator()(int x,int y){
return x + y;
}
};
void test01(){
Complex complex;
cout << complex.Add(10,20) << endl;
//对象当做函数来调用
cout << complex(10, 20) << endl;
}
14.8不能重载&&、||
不能重载operator&& 和 operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了--而且能够保证不需要。我们都已经习惯这种方便的特性了。
我们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。
class Complex{
public:
Complex(int flag){
this->flag = flag;
}
Complex& operator+=(Complex& complex){
this->flag = this->flag + complex.flag;
return *this;
}
bool operator&&(Complex& complex){
return this->flag && complex.flag;
}
public:
int flag;
};
int main(){
Complex complex1(0); //flag 0
Complex complex2(1); //flag 1
//原来情况,应该从左往右运算,左边为假,则退出运算,结果为假
//这边却是,先运算(complex1+complex2),导致,complex1的flag变为complex1+complex2的值, complex1.a = 1
// 1 && 1
//complex1.operator&&(complex1.operator+=(complex2))
if (complex1 && (complex1 += complex2)){
cout << "真!" << endl;
}
else{
cout << "假!" << endl;
}
return EXIT_SUCCESS;
}
根据内置&&的执行顺序,我们发现这个案例中执行顺序并不是从左向右,而是先右后左,这就是不满足我们习惯的特性了。由于complex1 += complex2先执行,导致complex1 本身发生了变化,初始值是0,现在经过+=运算变成1,1 && 1输出了真。
14.9 符号重载总结
- =, [], () 和 -> 操作符只能通过成员函数进行重载
- << 和 >>只能通过全局函数配合友元函数进行重载
- 不要重载 && 和 || 操作符,因为无法实现短路规则
常规建议
例 :字符串类封装
MyString.h
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include <iostream>
using namespace std;
class MyString
{
friend ostream& operator<< (ostream & out, MyString& str);
friend istream& operator>>(istream& in, MyString& str);
public:
MyString(const char *);
MyString(const MyString&);
~MyString();
char& operator[](int index); //[]重载
//=号重载
MyString& operator=(const char * str);
MyString& operator=(const MyString& str);
//字符串拼接 重载+号
MyString operator+(const char * str );
MyString operator+(const MyString& str);
//字符串比较
bool operator== (const char * str);
bool operator== (const MyString& str);
private:
char * pString; //指向堆区空间
int m_Size; //字符串长度 不算'\0'
};
MyString.cpp
#include "MyString.h"
//左移运算符
ostream& operator<< (ostream & out, MyString& str)
{
out << str.pString;
return out;
}
//右移运算符
istream& operator>>(istream& in, MyString& str)
{
//先将原有的数据释放
if (str.pString != NULL)
{
delete[] str.pString;
str.pString = NULL;
}
char buf[1024]; //开辟临时的字符数组,保存用户输入内容
in >> buf;
str.pString = new char[strlen(buf) + 1];
strcpy(str.pString, buf);
str.m_Size = strlen(buf);
return in;
}
//构造函数
MyString::MyString(const char * str)
{
this->pString = new char[strlen(str) + 1];
strcpy(this->pString, str);
this->m_Size = strlen(str);
}
//拷贝构造
MyString::MyString(const MyString& str)
{
this->pString = new char[strlen(str.pString) + 1];
strcpy(this->pString, str.pString);
this->m_Size = str.m_Size;
}
//析构函数
MyString::~MyString()
{
if (this->pString!=NULL)
{
delete[]this->pString;
this->pString = NULL;
}
}
char& MyString::operator[](int index)
{
return this->pString[index];
}
MyString& MyString::operator=(const char * str)
{
if (this->pString != NULL){
delete[] this->pString;
this->pString = NULL;
}
this->pString = new char[strlen(str) + 1];
strcpy(this->pString, str);
this->m_Size = strlen(str);
return *this;
}
MyString& MyString::operator=(const MyString& str)
{
if (this->pString != NULL){
delete[] this->pString;
this->pString = NULL;
}
this->pString = new char[strlen(str.pString) + 1];
strcpy(this->pString, str.pString);
this->m_Size = str.m_Size;
return *this;
}
MyString MyString::operator+(const char * str)
{
int newsize = this->m_Size + strlen(str) + 1;
char *temp = new char[newsize];
memset(temp, 0, newsize);
strcat(temp, this->pString);
strcat(temp, str);
MyString newstring(temp);
delete[] temp;
return newstring;
}
MyString MyString::operator+(const MyString& str)
{
int newsize = this->m_Size + str.m_Size + 1;
char *temp = new char[newsize];
memset(temp, 0, newsize);
strcat(temp, this->pString);
strcat(temp, str.pString);
MyString newstring(temp);
delete[] temp;
return newstring;
}
bool MyString::operator==(const char * str)
{
if (strcmp(this->pString, str) == 0 && strlen(str) == this->m_Size){
return true;
}
return false;
}
bool MyString::operator==(const MyString& str)
{
if (strcmp(this->pString, str.pString) == 0 && str.m_Size == this->m_Size){
return true;
}
return false;
}
TestMyString.cpp
void test01()
{
MyString str("hello World");
cout << str << endl;
//cout << "请输入MyString类型字符串:" << endl;
//cin >> str;
//cout << "字符串为: " << str << endl;
//测试[]
cout << "MyString的第一个字符为:" << str[0] << endl;
//测试 =
MyString str2 = "^_^";
MyString str3 = "";
str3 = "aaaa";
str3 = str2;
cout << "str2 = " << str2 << endl;
cout << "str3 = " << str3 << endl;
//测试 +
MyString str4 = "我爱";
MyString str5 = "北京";
MyString str6 = str4 + str5;
MyString str7 = str6 + "天安门";
cout << str7 << endl;
//测试 ==
if (str6 == str7)
{
cout << "s6 与 s7相等" << endl;
}
else
{
cout << "s6 与 s7不相等" << endl;
}
}
14.10附录:运算符和结合性
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | -- |
() | 圆括号 | (表达式)/函数名(形参表) | -- | ||
. | 成员选择(对象) | 对象.成员名 | -- | ||
-> | 成员选择(指针) | 对象指针->成员名 | -- | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
~ | 按位取反运算符 | ~表达式 | |||
++ | 自增运算符 | ++变量名/变量名++ | |||
-- | 自减运算符 | --变量名/变量名-- | |||
* | 取值运算符 | *指针变量 | |||
& | 取地址运算符 | &变量名 | |||
! | 逻辑非运算符 | !表达式 | |||
(类型) | 强制类型转换 | (数据类型)表达式 | -- | ||
sizeof | 长度运算符 | sizeof(表达式) | -- | ||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | |||
% | 余数(取模) | 整型表达式%整型表达式 | |||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | |||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | |||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | |||
< | 小于 | 表达式<表达式 | |||
<= | 小于等于 | 表达式<=表达式 | |||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | |||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | -- |
/= | 除后赋值 | 变量/=表达式 | -- | ||
*= | 乘后赋值 | 变量*=表达式 | -- | ||
%= | 取模后赋值 | 变量%=表达式 | -- | ||
+= | 加后赋值 | 变量+=表达式 | -- | ||
-= | 减后赋值 | 变量-=表达式 | -- | ||
<<= | 左移后赋值 | 变量<<=表达式 | -- | ||
>>= | 右移后赋值 | 变量>>=表达式 | -- | ||
&= | 按位与后赋值 | 变量&=表达式 | -- | ||
^= | 按位异或后赋值 | 变量^=表达式 | -- | ||
|= | 按位或后赋值 | 变量|=表达式 | -- | ||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | -- |
15 继承和派生
网页类
class IndexPage{
public:
//网页头部
void Header(){
cout << "网页头部!" << endl;
}
//网页左侧菜单
void LeftNavigation(){
cout << "左侧导航菜单!" << endl;
}
//网页主体部分
void MainBody(){
cout << "首页网页主题内容!" << endl;
}
//网页底部
void Footer(){
cout << "网页底部!" << endl;
}
private:
string mTitle; //网页标题
};
#if 0
//如果不使用继承,那么定义新闻页类,需要重新写一遍已经有的代码
class NewsPage{
public:
//网页头部
void Header(){
cout << "网页头部!" << endl;
}
//网页左侧菜单
void LeftNavigation(){
cout << "左侧导航菜单!" << endl;
}
//网页主体部分
void MainBody(){
cout << "新闻网页主体内容!" << endl;
}
//网页底部
void Footer(){
cout << "网页底部!" << endl;
}
private:
string mTitle; //网页标题
};
void test(){
NewsPage* newspage = new NewsPage;
newspage->Header();
newspage->MainBody();
newspage->LeftNavigation();
newspage->Footer();
}
#else
//使用继承,可以复用已有的代码,新闻业除了主体部分不一样,其他都是一样的
class NewsPage : public IndexPage{
public:
//网页主体部分
void MainBody(){
cout << "新闻网页主主体内容!" << endl;
}
};
void test(){
NewsPage* newspage = new NewsPage;
newspage->Header();
newspage->MainBody();
newspage->LeftNavigation();
newspage->Footer();
}
#endif
int main(){
test();
return EXIT_SUCCESS;
}
15.1 继承基本概念
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
派生类中的成员,包含两大部分:
- 一类是从基类继承过来的,一类是自己增加的成员。
- 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
15.2 派生类定义
派生类定义格式:
Class 派生类名 : 继承方式 基类名{
//派生类新增的数据成员和成员函数
}
三种继承方式:
- public : 公有继承
- private : 私有继承
- protected : 保护继承
从继承源上分:
- 单继承:指每个派生类只直接继承了一个基类的特征
- 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征
15.3派生类访问控制
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
派生类的访问权限规则如下:
//基类
class A{
public:
int mA;
protected:
int mB;
private:
int mC;
};
//1. 公有(public)继承
class B : public A{
public:
void PrintB(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
class SubB : public B{
void PrintSubB(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
void test01(){
B b;
cout << b.mA << endl; //可访问基类public属性
//cout << b.mB << endl; //不可访问基类protected属性
//cout << b.mC << endl; //不可访问基类private属性
}
//2. 私有(private)继承
class C : private A{
public:
void PrintC(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
class SubC : public C{
void PrintSubC(){
//cout << mA << endl; //不可访问基类public属性
//cout << mB << endl; //不可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
void test02(){
C c;
//cout << c.mA << endl; //不可访问基类public属性
//cout << c.mB << endl; //不可访问基类protected属性
//cout << c.mC << endl; //不可访问基类private属性
}
//3. 保护(protected)继承
class D : protected A{
public:
void PrintD(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
class SubD : public D{
void PrintD(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
void test03(){
D d;
//cout << d.mA << endl; //不可访问基类public属性
//cout << d.mB << endl; //不可访问基类protected属性
//cout << d.mC << endl; //不可访问基类private属性
}
15.4继承中的构造和析构
在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:
class Aclass{
public:
int mA;
int mB;
};
class Bclass : public Aclass{
public:
int mC;
};
class Cclass : public Bclass{
public:
int mD;
};
void test(){
cout << "A size:" << sizeof(Aclass) << endl;
cout << "B size:" << sizeof(Bclass) << endl;
cout << "C size:" << sizeof(Cclass) << endl;
}
15.5对象构造和析构的调用原则
- 继承中的构造和析构
- 子类对象在创建时会首先调用父类的构造函数
- 父类构造函数执行完毕后,才会调用子类的构造函数
- 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
- 析构函数调用顺序和构造函数相反
class A{
public:
A(){
cout << "A类构造函数!" << endl;
}
~A(){
cout << "A类析构函数!" << endl;
}
};
class B : public A{
public:
B(){
cout << "B类构造函数!" << endl;
}
~B(){
cout << "B类析构函数!" << endl;
}
};
class C : public B{
public:
C(){
cout << "C类构造函数!" << endl;
}
~C(){
cout << "C类析构函数!" << endl;
}
};
void test(){
C c;
}
继承与组合混搭的构造和析构
class D{
public:
D(){
cout << "D类构造函数!" << endl;
}
~D(){
cout << "D类析构函数!" << endl;
}
};
class A{
public:
A(){
cout << "A类构造函数!" << endl;
}
~A(){
cout << "A类析构函数!" << endl;
}
};
class B : public A{
public:
B(){
cout << "B类构造函数!" << endl;
}
~B(){
cout << "B类析构函数!" << endl;
}
};
class C : public B{
public:
C(){
cout << "C类构造函数!" << endl;
}
~C(){
cout << "C类析构函数!" << endl;
}
public:
D c;
};
void test(){
C c;
}
15.6 继承中同名成员的处理方法
- 当子类成员和父类成员同名时,子类依然从父类继承同名成员
- 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
- 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
class Base{
public:
Base():mParam(0){}
void Print(){ cout << mParam << endl; }
public:
int mParam;
};
class Derived : public Base{
public:
Derived():mParam(10){}
void Print(){
//在派生类中使用和基类的同名成员,显示使用类名限定符
cout << Base::mParam << endl;
cout << mParam << endl;
}
//返回基类重名成员
int& getBaseParam(){ return Base::mParam; }
public:
int mParam;
};
int main(){
Derived derived;
//派生类和基类成员属性重名,子类访问成员默认是子类成员
cout << derived.mParam << endl; //10
derived.Print();
//类外如何获得基类重名成员属性
derived.getBaseParam() = 100;
cout << "Base:mParam:" << derived.getBaseParam() << endl;
return EXIT_SUCCESS;
}
注意: 如果重新定义了基类中的重载函数,将会发生什么?
class Base{
public:
void func1(){
cout << "Base::void func1()" << endl;
};
void func1(int param){
cout << "Base::void func1(int param)" << endl;
}
void myfunc(){
cout << "Base::void myfunc()" << endl;
}
};
class Derived1 : public Base{
public:
void myfunc(){
cout << "Derived1::void myfunc()" << endl;
}
};
class Derived2 : public Base{
public:
//改变成员函数的参数列表
void func1(int param1, int param2){
cout << "Derived2::void func1(int param1,int param2)" << endl;
};
};
class Derived3 : public Base{
public:
//改变成员函数的返回值
int func1(int param){
cout << "Derived3::int func1(int param)" << endl;
return 0;
}
};
int main(){
Derived1 derived1;
derived1.func1();
derived1.func1(20);
derived1.myfunc();
cout << "-------------" << endl;
Derived2 derived2;
//derived2.func1(); //func1被隐藏
//derived2.func1(20); //func2被隐藏
derived2.func1(10,20); //重载func1之后,基类的函数被隐藏
derived2.myfunc();
cout << "-------------" << endl;
Derived3 derived3;
//derived3.func1(); 没有重新定义的重载版本被隐藏
derived3.func1(20);
derived3.myfunc();
return EXIT_SUCCESS;
}
- Derive1 重定义了Base类的myfunc函数,derive1可访问func1及其重载版本的函数。
- Derive2通过改变函数参数列表的方式重新定义了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问
- Derive3通过改变函数返回类型的方式重新定义了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏。
任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.
15.7非自动继承的函数
不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。
15.8继承中的静态成员特性
静态成员函数和非静态成员函数的共同点:
- 他们都可以被继承到派生类中。
- 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
- 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
class Base{
public:
static int getNum(){ return sNum; }
static int getNum(int param){
return sNum + param;
}
public:
static int sNum;
};
int Base::sNum = 10;
class Derived : public Base{
public:
static int sNum; //基类静态成员属性将被隐藏
#if 0
//重定义一个函数,基类中重载的函数被隐藏
static int getNum(int param1, int param2){
return sNum + param1 + param2;
}
#else
//改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
static void getNum(int param1, int param2){
cout << sNum + param1 + param2 << endl;
}
#endif
};
int Derived::sNum = 20;
15.9多继承
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
class Base1{
public:
void func1(){ cout << "Base1::func1" << endl; }
};
class Base2{
public:
void func1(){ cout << "Base2::func1" << endl; }
void func2(){ cout << "Base2::func2" << endl; }
};
//派生类继承Base1、Base2
class Derived : public Base1, public Base2{};
int main(){
Derived derived;
//func1是从Base1继承来的还是从Base2继承来的?
//derived.func1();
derived.func2();
//解决歧义:显示指定调用那个基类的func1
derived.Base1::func1();
derived.Base2::func1();
return EXIT_SUCCESS;
}
多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
解决方法就是显示指定调用那个基类的版本。
菱形继承和虚继承
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:
- 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
- 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{
public:
BigBase(){ mParam = 0; }
void func(){ cout << "BigBase::func" << endl; }
public:
int mParam;
};
class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};
int main(){
Derived derived;
//1. 对“func”的访问不明确
//derived.func();
//cout << derived.mParam << endl;
cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;
//2. 重复继承
cout << "Derived size:" << sizeof(Derived) << endl; //8
return EXIT_SUCCESS;
}
上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:
class BigBase{
public:
BigBase(){ mParam = 0; }
void func(){ cout << "BigBase::func" << endl; }
public:
int mParam;
};
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
class Derived : public Base1, public Base2{};
int main(){
Derived derived;
//二义性问题解决
derived.func();
cout << derived.mParam << endl;
//输出结果:12
cout << "Derived size:" << sizeof(Derived) << endl;
return EXIT_SUCCESS;
}
以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。
虚继承实现原理
class BigBase{
public:
BigBase(){ mParam = 0; }
void func(){ cout << "BigBase::func" << endl; }
public: int mParam;
};
#if 0 //虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
#else //普通继承
class Base1 : public BigBase{};
class Base2 : public BigBase{};
#endif
class Derived : public Base1, public Base2{};
普通继承 | 虚继承 | |
BigBase: | ||
Base1: | ||
Base2: | ||
Derived: |
通过内存图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。
- BigBase 菱形最顶层的类,内存布局图没有发生改变。
- Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
- Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。
由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 Derived三个类对象共享了一份BigBase数据。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
class BigBase{
public:
BigBase(int x){mParam = x;}
void func(){cout << "BigBase::func" << endl;}
public:
int mParam;
};
class Base1 : virtual public BigBase{
public:
Base1() :BigBase(10){} //不调用BigBase构造
};
class Base2 : virtual public BigBase{
public:
Base2() :BigBase(10){} //不调用BigBase构造
};
class Derived : public Base1, public Base2{
public:
Derived() :BigBase(10){} //调用BigBase构造
};
//每一次继承子类中都必须书写初始化语句
int main(){
Derived derived;
return EXIT_SUCCESS;
}
注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的. |
工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。