sizeof()是一个运算符,不是一个函数
看程序效率的快慢,可以直接看其汇编语言程序的多少
扩展名:
c语言:c
c++:cpp
Java:先有类,再有方法
c++完全兼容c语法
getchar()等待键盘输入(如果敲回车,就会读取键盘输入)
函数重载(overload)
-
c语言不支持函数重载
-
两个函数的函数名完全相同,但是函数参数类型,个数,顺序不同
返回值类型不同,不能构成重载 -
隐式转换(小转大)也可在函数参数类型中适用,但要注意隐式转换有可能产生二义性。
本质:
采用了name mangling 或者叫 name decoration技术
C++编译器默认会对符号(比如函数名)进行改编、修饰
重载时会生成多个不同的函数名,不同编译器(MSVC、g++)有不同的生成规则
IDA pro 逆向工程 软件
Debug模式:很多调试信息
Release模式:去除调试信息,生成的可执行文件比较精简、高效,会默认对函数进行优化
visual stdio 2019可选择不对其进行优化
默认参数
C++允许函数设置默认参数,在调用时可以根据情况省略实参。规则如下:
· 默认参数只能按照右到左的顺序
· 如果函数同时有声明、实现,默认参数只能放在函数声明中
· 默认参数的值可以是常量、全局符号(全局变量、函数名)
void(*p)(int) = test; //此语句的作用就是更换test函数的名称,其中void指test函数的返回类型,int指test函数中参数类型,p为将要命名的函数名称。
p(10);
#include <iostream>
using namespace std;
void func(int v1, void(*p)(int)=test)
{
p(v1);
}
void test(int a)
{
cout << "test(int)-" << a << endl;
}
int main()
{
func(30);
func(10, test);
return 0;
}
如果函数的实参经常是一个值,可以考虑使用默认参数
函数重载 、默认参数可能会产生冲突、二义性(建议优先选择使用默认参数)
例:
void display(int a,int b=20)
{
cout<<"a is"<<a<<endl;
}
void display(int a)
{
cout<<"a is"<<a<<endl;
}
int main()
{
display(10);//该语句会产生二义性
return 0;
}
默认参数的本质
自动补充参数
extern “C”
被extern "C"修饰的代码会按照c语言的方式编译
#include <iostream>
using namespace std;
extern "C" void func()
{
}
extern "C" void func(int v)
{
}
int main()
{
return 0;
}
写法
extern "C"
{
void func()
{
}
void func(int v)
{
}
}
extern "C" void func()
{
}
注:以上代码报错,因为C语言中不支持函数重载
如果函数同时有声明和实现,要让函数声明被extern “C”修饰,函数实现可以不修饰。
#include <iostream>
using namespace std;
extern "C" void func();
int mian()
{
return 0;
}
extern "C" void func()
{
}
**如果相同的函数以不同的语言编译,所代表的函数不相同**
#include <iostream>
using namespace std;
void func();
extern "C" void func(int v);//除去参数V报错原因与重载无关,是二义性
int main()
{
func();
func(10);
return 0;
}
void func()
{
cout << "func()" << endl;
}
void func(int v)
{
cout << "func(int v)-"<<v << endl;
}
externC2-C、C++混合开发
由于C、C++编译规则的不同,再C、C++混合开发时,可能会经常出现一下操作
extern 无法在C语言中使用
每个C++文件之前都隐含着一个定义 #define __cplusplus
可根据这个特点来使头文件在不同的语言文件中判断是否添加extern ”C”
如果定义了 __cplusplus这个宏,则extern "C"参与编译
math.h头文件的定义
#ifdef __cplusplus
extern "C"{
#endif
int sum(int v1, int v2);
int delta(int v2, int v3);
int divide(int v1, int v2);
#ifdef __cplusplus
}
#endif
C++可以不导入头文件,但必须要写声明,头文件其实就是声明
重复包含头文件,浪费编译器工作时间
为了防止重复包含头文件**#ifndef…#endif**
#ifndef 1 //为了防止重复定义头文件
#ifdef __cplusplus
extern "C" {
#endif
int sum(int v1, int v2);
int delta(int v2, int v3);
int divide(int v1, int v2);
#ifdef __cplusplus
}
#endif
#endif // !1
注意:防止重复包含头文件的宏都是唯一的,一般使用__文件名
pragma once
我们经常使用#ifndef、#define、#endif来防止头文件的内容被重复包含
#pragma once可以防止整个文件的内容被重复包含
使用:放在头文件中最前面
区别:
1.#ifndef、#define、#endif受C\C++标准的支持,不受编译器的任何限制
2.有些编译器不支持#pragma once(较老编译器不支持,如GCC3.4版本之前),兼容性不够好
3.#ifndef、#define、#endif可以针对一个文件中的部分代码,而#pragma once只能针对整个文件
内联函数(inline function)
使用inline修饰的函数的声明或者实现,可以使其变为内联函数**
将函数展开为函数体代码
#include <iostream>
using namespace std;
inline int sum(int v1, int v2); //将函数调用展开为函数体代码
inline void func()
{
cout << "func()" << endl;
cout << "func()1" << endl;
}
int main()
{
func();
/*
这里的func()相当于
cout << "func()" <<endl;
*/
int c = sum(10, 20);
/*
这里相当于
int c = 10 + 20;
*/
cout << c << endl;
return 0;
}
inline int sum(int v1, int v2)
{
return v1 + v2;
}
意义:
函数调用需要开辟栈空间,回收栈空间,内联函数则不存在栈空间的开辟,不需要回收内存
什么时候使用内联函数
适用于函数体积小
经常调用的函数
特点
编译器会将函数调用直接展开为函数体代码
可以减少函数调用的开销
会增大代码体积
注意
尽量不要内联超过10行代码的函数
有些函数即使声明为inline,也不一定会被编译器内联,比如递归函数
内联函数-----宏
#define add(v1,v2) v1+v2
int main()
{
cout << add(10, 20) << endl;
return 0;
}
对比:
内联函数和宏,都可以减少函数调用的开销
对比宏,内联函数多了语法检测和函数特性
#define add(v) v+v
inlinde int add1(int v)
{
return v+v;
}
int main()
{
int a=10;
cout<<add(++a)<<endl;
//结果为24,相当于cout << ++a + ++a <<endl;
cout<< add1(++a)<<endl;
//结果为22
return 0;
}
表达式
C++有些表达式可以被赋值
int a=1;
int b=2;
(a=b)=4;
//这里a=4,b=2
const
const 是常量的意思,被其修饰的变量不可修改
如果修饰的是类、结构体(的指针),其成员也不可修改
#include <iostream>
using namespace std;
struct Data
{
int year;
int month;
int day;
};
int main()
{
const int age = 10;
const Data d = { 1200, 23, 2 };
d.year = 15;
cout << d.year << endl;//输出错误
return 0;
}
以下5个指针是什么含义?
int age=10;
const int *p1=&age; //*p1是常量,p1可以改变
int const *p2=&age; //与p1没区别
int * const p3=&age; //p3是常量,不能改变,*p3可以改变
const int * const p4=&age; //与p5没区别 p4,*p4都是常量
int const *const p5=&age;
const修饰的是其右边的内容
struct Student {int age;};
Student stu1={10};
Student stu2={20};
const Student *pStu1=&stu1; //修饰 *pStu1
*pStu1 = stu2; //报错 直接访问
(*pStu1).age=30; //报错 间接访问
pStu1->age=30; //报错 间接访问
pStu1 = &stu2; //正确
Student * const pStu2 = &stu2; //修饰pStu2
*pStu2=stu1; //正确
(*pStu2).age=30; //正确
pStu2->age=30; //正确
pStu2=&stu1; //报错
引用(Reference)
在c语言中,使用指针间接修改某个变量的值。
int age = 10;
int height = 20;
//定义了一个age的引用
int& ref = age;
int& ref1=ref;
int& ref2=ref1;
ref = 20;
age += 30;
age = height;
age = 11;
cout << height << endl;
注意
引用相当于是变量的别名(基本数据类型、枚举、结构体、类、指针、数组等,都可以由引用)
对引用做计算,就是对引用所指向的变量做计算
在定义的时候就必须初始化,一旦指向了某个变量,就不可以再改变,“从一而终”
可以利用引用初始化另一个引用,相当于某个变量的多个别名
不存在【引用的引用、指向引用的指针、引用数组】
引用的价值
比指针更安全、函数返回值可以被赋值
void swap(int &v1, int &v2)
{
int tmp = v1;
v1 = v2;
v2 = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
cout << "a=" << a << ",b=" << b << endl;
return 0;
}
参数的引用是栈里的
引用的本质:
引用的本质就是指针,只是编译器削弱了它的功能,所及引用就是弱化的指针
int age=10;
//*p就是age的别名
int *p=&age;
*p=30;
//ref就是age的别名
int &ref=age;
ref=40;
指针的大小和环境有关(X86,X64)
ASLR 让应用的起始地址是随机的
汇编
汇编语言的种类
8086汇编(16bit)
X86汇编(32bit)
X64汇编(64bit)
ARM汇编(嵌入式、移动设备)
X64汇编根据编译器的不同,有两种书写格式
Intel
AT&T
汇编语言不区分大小写
学习汇编语言2大知识点:
1.汇编指令
2.寄存器
寄存器
通常,CPU会先将内存中的数据存储到寄存器中,然后再对寄存器中的数据进行运算。
64bit:
通用寄存器:RAX \ RBX \ RCX \ RDX;
寄存器大小和环境有关,64位为8个字节
32bit:
通用寄存器:EAX \ EBX \ECX \EDX;
16bit:
通用寄存器:AX \ BX \ CX \DX
一般规律:
R开头的寄存器是64bit的,占8个字节
**imm:**立即数
内联汇编
#include <iostream>
using namespace std;
int main()
{
int a = 10;
__asm
{
mov eax,a
}
return 0;
}
mov dest,src
将src的内容赋值给dest,类似于dest=src
[地址值]
1.中括号[]里面放的都是内存地址
2.word 是2字节,dword是4字节(double word),qword是8字节 (quard word)
call 函数地址
add op1,op2
类似于op1=op1+op2
sub op1,op2
类似于op1=op1-op2
inc op
自增,类似于op=op+1
dec op
自减,类似于op=op-1
jmp 内存地址
跳转到某个内存地址去执行代码
j开头的一般都是跳转,大多数是带条件的跳转,一般跟test、cmp等指令配合使用
指针的汇编特征
lea eax,[ebp-0Ch]
mov dword ptr [ebp-18h],eax
引用的汇编特征
与指针相同
引用的补充
//结构体的引用
#include <iostream>
using namespace std;
struct Data
{
int year;
int month;
int day;
};
int main()
{
Data d = { 2021,07,20 };
Data& ref = d;
d.day = 2013;
cout << d.day << endl;
return 0;
}
//指针的引用
int age = 10;
int* p = &age;
int* &ref = p;//引用的 数据类型是int*
*ref = 30;
int height=30;
ref=&height;//更改ref的地址
数组名array其实是数组的地址,也是数组首元素的地址
数组名array可以看做是指向数组首元素的指针(int *)
//数组的引用
int array[]={1,2,3};
//法一:
int (&ref)[3]=array;
ref[0]=10;
//法二:
int * const &ref = array;
指针数组
数组里面可以存放3个int*
*int p;
*int arr1[3]={p,p,p}
用于指向数组的指针
指向存放3个整数的数组
int (*arr2)[3];
常引用(const Reference)
引用可以被const修饰,这样就无法通过引用修改数据了,可以称为常引用
const必须写在&符号的左边,才算是常引用
int age=10;
const int &ref=age;
ref=30; //这时就不能更改其值,
意义:
只读,阻止其他函数修改所引用的值
int func(const int &a);
const int &ref=age;//与下面相同
int const &ref=age;//ref不可以修改指向,且不可以通过ref间接修改所指向的变量
int &const ref=age;//ref不能修改指向,但是可以通过ref间接修改所指向的变量 **没有意义**
相当于int * const ref=age;
const引用的特点
可以指向临时数据(常量、表达式、函数返回值等)
可以指向不同类型的数据
int age = 10;
int a=1;
int b=2;
const int &ref1 = a + b;
const int &ref = 30;
const double *ref2 = age;
作为函数参数时(此规则也适用于const指针)
可以接受const和非const实参(非const引用,只能接受非const实参)
可以跟非const引用构成重载
int sum(const int &v1,const int &v2)
{
return v1 + v2;
}
int sum(int &v1,int &v2)
{
return v1 + v2;
}
int main()
{
int a = 10;
int b = 20;
sum(a,b);
const int c = 10;
const int d = 20;
sum(c,d);
sum(10,20);//重要作用
}
当常引用指向了不同类型的数据时,就会产生临时变量,即引用指向的并不是初始化时的那个变量
int age = 10;
const int &rAge = age;
age = 30;
cout << " age is " << age <<endl; 30
cout << " rAge is " << rAge <<endl; 30
int age = 10;
const double &rAge = age;
age = 30;
cout << " age is " << age <<endl; 30
cout << " rAge is " << rAge <<endl; 10
面向对象
类和对象
类
C++可以使用struct、class来定义一个类
使用struct定义类
#include <iostream>
using namespace std;
struct Person
{
int age; //成员变量
void fun()//成员函数(方法)
{
cout << "Person :: run()"<<age << endl;
}
};
int main()
{
//利用类创建对象
Person person; //这个对象的内存在栈空间
person.age = 10;
person.fun();
return 0;
}
struct 和 class的区别
struct的默认成员权限是public
class的默认诚邀您权限是private
命名规范
1.全局变量:g_
2.成员变量:m_
3.静态变量:s_
4.常量:c_
通过指针间接访问person对象
#include <iostream>
using namespace std;
struct Person
{
int age; //成员变量
void fun()//成员函数(方法)
{
cout << "Person :: run()"<<age << endl;
}
};
int main()
{
Person* p = &person;
p->age = 20;
p->fun();
return 0;
}
上面代码中的person 对象、p指针的内存都是在函数的栈空间,自动分配和回收的
64位环境下,person占用4个字节(Person类中只有一个int变量),p占用8个字节(64位环境下,地址占用8个字节)
实际开发中,使用class比较多
this this就是指针
this指针存储着函数调用者的地址
this指向了函数的调用者
#include <iostream>
using namespace std;
struct Person
{
int m_age;
void run()
{
cout << "Person::run()" << this->m_age << endl;//this是隐式参数
}
};
int main()
{
Person person;
person.m_age = 21;
person.run();
return 0;
}
作用:可以使不同的对象,调用同一个函数。
指针访问的本质
lea eax,[ebp-14h]
mov dword ptr [ebp-20h],eax
mov eax,dword ptr[ebp-20h]
mov dword ptr [eax],0Ah
mov eax,dword ptr [ebp-20h]
mov dword ptr [eax],0Ah
原理:如何利用指针间接访问所指向对象的成员变量?
1.从指针中取出对象的地址
2.利用对象的地址 + 成员变量的偏移量计算出成员变量的地址
3.根据成员变量的地址访问成员变量的存储空间
思考:最后打印出来的 每个成员变量值是多少?将person.display()换成p->display()成员值会变成多少?
struct Person
{
int m_id;
int m_age;
int m_height;
void display()
{
//eax == &person.m_age == &person+4
//mov eax,dword ptr [this]
//[eax],[eax + 4],[eax + 8]
//[&person + 4],[&person +4 + 4],[&person + 4 + 8]
cout<<" id= "<< m_id
<<" ,age ="<<m_age
<<",height = "<<m_height<<endl;
}
}
Person person;
person.m_id=10;
person.m_age=20;
person.m_height=30;
Person *p=(Person *) &person.m_age;
//eax == &person.m_age == &person + 4
//mov eax,dword ptr[p]
//mov dword ptr [eax + 0],40
//mov dword ptr [&person + 4 + 0],40
p->m_id=40;
//mov dword ptr [eax + 4],50
//mov dword ptr [&person + 4 + 4],50
p->m_age=50;
//将person对象的地址传递给display函数的this
person.display();
//会将指针p里面存储的地址传递给display函数的this
//将&person.m_age传递给display函数的this
p->display();
答案:10,40,50;
40,50,xxx
0xcc
函数所在栈空间在使用之前存在垃圾数据,需要 用cc来填充
cc->对应 int3(中断):起到断点的作用
当其它语句不小心跳到函数栈空间时,就会在cc的作用下停止执行。
内存
封装
成员变量私有化,提供公共的getter和setter给外界区访问成员变量
#include <iostream>
using namespace std;
struct Person
{
private:
int m_age;
public:
void setAge(int age)
{
if (age <= 0) return;
m_age = age;
}
int getAge()
{
return m_age;
}
};
int main()
{
Person person;
person.setAge(4);
cout << person.getAge() << endl;
return 0;
}
内存空间的布局
每个应用都有自己独立的内存空间,其内存空间一般都有以下几大部分
代码段(代码区)
用于存放代码,只读
数据段(全局区)
用于存放全局变量等
栈空间
每调用一个函数就会给它分配一段连续的栈空间,等函数调用 完毕后会自动回收这段栈空间
自动分配和回收
堆空间
需要主动申请和释放
在程序运行过程中,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存。
堆空间的申请和释放
malloc/free
#include <iostream>
using namespace std;
void test()
{
int* p = (int*)malloc(4);
*p = 10;
free(p);
}
void test2()
{
char *p=(char *)malloc(4);
p[0]=1;
p[1]=2;
p[2]=3;
p[3]=4;
/*
*p=10;
*(p+1)=11;
*(p+2)=12;
*(p+3)=13;
*/
free(p);//回收所开辟所有空间
}
int main()
{
test();
return 0;
}
在X86环境中
int *p =(int *)malloc(4);
*p=10;
指针p在栈空间中,其占四个字节,存储的是堆空间所开辟的4个字节空间的首地址。
函数调用完毕后栈空间会自动消失,但是堆空间还在,需要free去释放
new/delete
int* p = new int;
*p = 10;
delete p;
new[]/delete[]
char* p = new char[4];
delete[] p;
注意
申请堆空间成功后,会返回那一段内存空间的地址
申请和释放必须是1对1的关系,不然可能会存在内存泄漏
现在很多高级编程语言不需要开发人员去管理内存(比如Java),屏蔽了很多内存细节,利弊同时存在
利:提高开发效率,避免内存使用不当或泄漏
弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手
**堆空间的初始化 **
memory set
int size = sizeof(int) * 10;
int* p = (int*)malloc(size);
memset(p, 0, 40);
//从p地址开始,将40个字节中每一个字节设置为0;
int *p0 = new int; //没有初始化
int *p1 = new int(); //初始化为0
int *p2 = new int(5); //初始化为5
int *p3 = new int[3]; //数组元素未被初始化
int *p4 = new int[3](); //3个数组元素都被初始化为0
int *p5 = new int[3]{}; //3个数组元素都被初始化为0
int *p6 = new int[3]{5};//数组的首元素被初始化为5,其他为0
memset函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法
Person person;
person.m_id=1;
person.m_age=20;
person.m_height=180;
memset(&person,0,sizeof(person));
Person persons[]={{1,20,180},{2,25,165},{3,27,170}};
memset(persons,0,sizeofz(persons));
对象的内存
对象的内存可以存在于3种地方
全局区(数据段):全局变量
栈空间:函数里面的局部变量
堆空间:动态申请内存(malloc、new等)
//全局区
Person g_person;
int main()
{
//栈空间
Person person;
//堆空间
Person *p=new Person;
return 0;
}
构造函数(Constructor)
构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作。
#include <iostream>
using namespace std;
struct Person
{
int m_age;
Person()
{
m_age = 0;
cout << "Person()" << endl;
}
Person(int age)
{
m_age = age;
cout << "Person(int age)" <<age<< endl;
}
};
int main()
{
Person person;
Person person2(20);
Person person3(30);
Person *p=(Person *)malloc(sizeof(Person));
p->m_age=10;
Person *p1 = new Person;
return 0;
}
特点
1.函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数
2.一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象
注意
栈空间和堆空间的创建对象,都会调用构造函数
通过malloc分配的对象,不会调用构造函数(malloc只会申请堆空间)
默认情况下,编译器会为每一个类生成空的无参的构造函数 错误的
在某些特定的情况下,编译器才会为类生成空的无参的构造函数
构造函数的调用
#include <iostream>
using namespace std;
struct Person
{
int m_age;
int m_money;
Person()
{
m_age = 0;
cout << "Person()" << endl;
}
Person(int age)
{
m_age = age;
cout << "Person(int age)" <<age<< endl;
}
};
Person g_person0; //Person()调用了无参
Person g_perosn1(); //函数声明
Person g_person2(20); //有参
int main()
{
Person person; //无参
Person person1(); //函数声明
Person person2(20); //有参
Person* p0 = new Person; //无参
Person* p1 = new Person(); //无参
Person* p2 = new Person(20); //有参
/*
4个无参,3个有参
*/
return 0;
}
成员变量的初始化
#include <iostream>
using namespace std;
struct Person
{
int m_age;
};
//全局区:成员变量初始化为0
Person g_person;
int main()
{
Person person;//栈空间:没有初始化成员变量
//堆空间:没有初始化成员变量
Person* p0 = new Person;
//堆空间:成员变量初始化为0(有构造函数时不会初始化,编译器认为构造函数会初始化变量)
Person* p1 = new Person();
Person *p4 = new Person[3]; //成员变量不会被初始化
Person *p5 = new Person[3](); //3个Person对象的成员变量都初始化为0(没有自定义构造函数)
Person *p6 = new Person[3]{}; //3个Person对象的成员变量都初始化为0(没有自定义构造函数)
cout << g_person.m_age << endl;
//cout << person.m_age << endl;
cout << p0->m_age << endl;
cout << p1->m_age << endl;
return 0;
}
如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化
初始化方法
struct Person
{
int m_age;
Person()
{
memset(this,0,sizeof(Person));
}
}
析构函数(Destructor)
析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作。
#include <iostream>
using namespace std;
struct Person
{
int m_age;
//新的Person对象诞生的过程
Person()
{
cout << "Person::Person()" << endl;
}
//一个Person销毁的象征
~Person()
{
cout << "~Person()" << endl;
}
};
int main()
{
{//作用域
Person person;
}
return 0;
}
特点
函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个
注意
通过malloc分配的对象free的时候不会调用析构函数
Person *p = (Person*)malloc(sizeof(Person));
free(p);//此时不会调用析构函数
构造函数、析构函数要声明为public,才能被外界正常使用
内存管理:
#include <iostream>
using namespace std;
struct Car
{
int m_price;
Car()
{
m_price = 0;
cout << "Car::Car()" << endl;
}
~Car()
{
cout << "Car::~Car()" << endl;
}
};
struct Person
{
int m_age;
Car* m_car;
//用来做初始化工作
Person()
{
m_age = 0;
m_car = new Car();//在堆空间,不会自动回收
cout << "Person::person()" << endl;
}
//用来做内存清理工作
~Person()
{
delete m_car;//这个位置适合
cout << "Person::~person()" << endl;
}
};
int main()
{
//内存泄漏:该释放的内存没有释放
{
Person person;
//delete person.m_car;//释放内存,但这样写太麻烦
}
return 0;
}
//当栈空间person被回收之后,堆空间中的car不会被回收,所以在person中的析构函数中回收car
对象内部申请的堆空间,由对象内部回收
声明和实现分离
具体参见实验
#include <iostream>
using namespace std;
class Person
{
private:
int m_age;
public:
void setAge(int age);
int getAge();
Person();
~Person();
};
void Person::setAge(int age)
{
m_age = age;
}
int Person::getAge()
{
return m_age;
}
Person::Person()
{
m_age = 0;
}
Person::~Person()
{
}
int main()
{
getchar();
return 0;
}
命名空间
可以避免命名冲突
#include <iostream>
using namespace std;
namespace MJ {
int g_age;//它是个全局变量
class Person
{
int m_money;
int m_age;
};
}
class Person
{
int m_height;
};
int main()
{
//法一
MJ::g_age=10;
MJ::Person person;
cout << sizeof(person) << endl;
//法二
using namespace MJ;
g_age=10;
Person person;
using MJ::g_age;//只使用某一个
getchar();
return 0;
}
命名空间不影响布局
思考:
namespace MJ
{
int g_age;
}
namespace FX
{
int g_age;
}
using namespace MJ;
using namespace FX;
//这句代码能通过编译么?
g_age=20;
//不能,具有二义性
命名空间的嵌套
namespace MJ
{
namespace SS
{
int g_age;
}
}
int main()
{
MJ::SS::g_age = 10;
using namespace MJ::SS;
g_age = 20;
using namespace MJ::SS::g_age;
g_age = 30;
return 0;
}
有个默认的全局命名空间,我们创建的空间默认嵌套在它里面
void func()
{
cout<<"func()"<<endl;
}
namespace MJ
{
void func()
{
cout<<"MJ::func()"<<endl;
}
};
int main()
{
using namespace MJ;
MJ::func();
::func();//使用全局命名空间
return 0;
}
命名空间的合并
namespace MJ
{
int g_age;
}
namespace MJ
{
int g_no;
}
//等价于
namespce MJ
{
int g_age;
int g_no;
}
实验 声明实现和分离
继承
可以让子类拥有父类所有成员函数。
#include <iostream>
using namespace std;
struct Person
{
int m_age;
void run(){}
};
struct Student:Person
{
int m_age;
int m_score;
void run() {}
void study() {}
};
struct Worker:Person
{
int m_age;
int m_salary;
void run(){}
};
int main()
{
return 0;
}
C++中没有基类
继承之后对象的内存布局
struct Person
{
int m_age;
};
struct Student:Person
{
int m_no;
};
struct GoodStudent:Student
{
int m_money;
}
int main()
{
Person person;//4个字节
Strdent stu; //8个字节
GoodStudent gs;//12个字节
}
在内存中父类的成员变量会排布在前面
成员访问权限:
成员访问权限、继承方式有3种:
public:公共的,任何地方都可以访问(struct默认)
protected:子类内部、当前类内部可以访问
private:私有的,只有当前类内部可以访问(class默认)
#include <iostream>
using namespace std;
struct Person
{
public:
int m_age;
void run()
{
}
};
struct Student :protected Person
{
void study()
{
m_age = 10;
}
};
struct GoodStudent :public Person
{
};
int main()
{
Person person;
person.m_age = 10;
}
子类内部访问父类成员的权限,是以下2项中权限最小的那个
成员本身的访问权限
上一级父类的继承方式
开发中用的最多的继承方式是public,这样能保留父类原来的成员访问权限
访问权限不影响对象的内存布局
初始化列表
#include <iostream>
using namespace std;
struct Person
{
int m_age;
int m_height;
/*Person(int age,int height)
{
m_age=age;
m_height=height;
}*/
//等价于
Person(int age,int height):m_age(age),m_height(height)//初始化列表
{
}
};
int main()
{
Person person(18,180);
getchar();
return 0;
}
特点
一种便捷的初始化成员变量的方式
只能用在构造函数中
初始化顺序只跟成员变量的声明顺序有关
初始化列表和默认参数的配合使用
struct Person
{
int m_age;
int m_height;
Person(int age=0,int height=0):m_age(age),m_height(height){}
};
int main()
{
Person person1;
Person person2(17);
Person person(18,180);
}
如果函数声明和实现是分离的,初始化列表只能写在函数的实现中
struct Person
{
int m_age;
int m_height;
Person(int age=0,int height=0)
};
Person(int age=0,int height=0):m_age(age),m_height(height){}
int main()
{
Person person1;
Person person2(17);
Person person(18,180);
}
构造函数的互相调用:
构造函数调用构造函数必须写在初始化列表当中,普通函数则不需要
struct Person
{
int m_age;
int m_height;
Person():Person(10,20){}
Person(int age,int height);
{
m_age = age;
m_height = height;
}
}
**注意:**下面的写法是错误的,初始化的是一个临时对象
struct Person
{
int m_age;
int m_height;
Person()
{
Person(0,0);
}
Person(int age,int height):m_age(age),m_height(height){}
}
父类的构造函数:
如果父类没有构造函数,子类不调用,如果有,子类则必须调用
#include <iostream>
using namespace std;
struct Person
{
int m_age;
Person()
{
cout<<"Person::person()"<<end;
}
Person(int age)
{
cout<<"Person::person(int age)"<<endl;
}
};
struct Student:Person
{
int m_no;
Student()
{
cout<<"Student::Student()"<<endl;
}
};
int main()
{
Student student;
return 0;
}
子类的构造函数会默认调用父类无参的构造函数
如果子类的构造函数显示的调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数
如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数
class Person
{
int m_age;
public:
Person(int age):m_age(age){}
};
class Student:Person
{
int m_no;
public:
Student(int age,int no):m_no(no),Person(age){}
}
int main()
{
Student student(18,34);
}
析构函数的调用
class Person
{
int m_age;
public:
Person()
{
cout<<"Person::Person()"<<endl;
}
~Person()
{
cout<<"Person::~Person()"<<endl;
}
};
class Student:Person
{
int m_no;
public:
Student()
{
//call Person::Person调用父类构造函数
cout<<"Student::Student()"<<endl;
}
~Student()
{
cout<<"Student::~student()"<<endl;
//call Person::~Person() 调用父类析构函数
}
}
int main()
{
{
Student student;
}
return 0;
}
多态
父类指针、子类指针
父类指针可以指向子类对象、是安全的,开发中经常用到(继承方式必须是public)
子类指针指向父类对象是不安全的
#include <iostream>
using namespace std;
struct Person
{
int m_age;
};
struct Student :Person
{
int m_score;
};
int main()
{
//父类子针指向子类对象,父类指针只能访问父类定义的变量,所以它是安全的
Person* p = new Student();
//而子类指针指向父类对象是不安全的,子类中的变量在父类中是不包含的
return 0;
}
默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态
多态是面向对象非常重要的一个特性
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
在运行时,可以识别出真正的对象类型,调用对应子类中的函数
多态的要素
子类重写父类的成员函数(override),成员函数必须是虚函数
父类指针指向子类对象
利用父类指针调用重写的成员函数
struct Animal
{
void speak()
{
cout<<"Animal::speak()"<<endl;
}
void run()
{
cout<<"Animal::run()"<<endl;
}
}
struct dog:Animal
{
//重写(覆写、覆盖、orrivade):要求返回值,函数名,参数要与父类相同
void speak()
{
cout<<"dog::speak()"<<endl;
}
void run()
{
cout<<"Animal::run()"<<endl;
}
}
struct cat:Animal
{
void speak()
{
cout<<"cat::speak()"<<endl;
}
void run()
{
cout<<"cat::run()"<<endl;
}
}
void liu(Animal *p)
{
p->speak();
p->run();
}
int main()
{
liu(new dog());//无法实现多态
cat *p=(cat *) new Dog();
p->speak();//call cat::speak
p->run();//call cat::run
return 0;
}
虚函数
C++中的多态通过虚函数(virtual function)来实现
虚函数:被virtual修饰的成员函数
只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual)
struct Animal
{
virtual void speak()
{
cout<<"Animal::speak()"<<endl;
}
virtual void run()
{
cout<<"Animal::run()"<<endl;
}
}
struct dog:Animal
{
//重写(覆写、覆盖、orrivade):要求返回值,函数名,参数要与父类相同
void speak()
{
cout<<"dog::speak()"<<endl;
}
void run()
{
cout<<"Animal::run()"<<endl;
}
}
struct cat:Animal
{
void speak()
{
cout<<"cat::speak()"<<endl;
}
void run()
{
cout<<"cat::run()"<<endl;
}
}
void liu(Animal *p)
{
p->speak();
p->run();
}
int main()
{
Animal *p=new cat();
p->speak();
p->run();
liu(new cat());
liu(new dog());
return 0;
}
虚表
虚函数的实现原理是虚表,这个虚表里面存储着最终要调用的虚函数地址,这个虚表也叫作虚函数表
class Animal
{
public:
int m_age;
virtual void speak()
{
cout<<"Animal::speak()"<<endl;
}
virtual void run()
{
cout<<"Animal::run()"<<endl;
}
};
class Cat:public Animal()
{
public:
int m_life;
void speak()
{
cout<<"Cat::speak()"<<endl;
}
void run()
{
cout<<"Cat::run()"<<endl;
}
};
int main()
{
Animal *cat = new Cat();
cat->m_age=20;
cat->speak();
cat->run();
return 0;
}
所有的Cat对象(不管在全局区、栈、堆)共用一份虚表
如果类中有虚函数,则定义该类对象时以右边new的对象类型为标准,如没有,则以左边对象类型为标准
多态使用的场合
一个父类下面包含了好几个子类,每个子类都重写了 父类中的方法
调用父类的成员函数
stuct Animal
{
virtual void speak()
{
cout<<"Animal::Speak()"<<endl;
}
virtual void run()
{
cout<<"Animal::run()"<<endl;
}
}
struct Cat::Animal
{
void speak()
{
Animal::speak();//调用父类的成员函数
}
}
虚析构函数
如果存在父类指针指向子类对象的时候,应该将析构函数声明为虚函数(虚析构函数)
delete父类 指针时,才会调用子类的析构函数,保证 析构的完整性
stuct Animal
{
virtual void speak()
{
cout<<"Animal::Speak()"<<endl;
}
virtual void run()
{
cout<<"Animal::run()"<<endl;
}
virtual ~Animal()
{
cout<<"Animal::~Animal"<<endl;
}
}
struct Cat::Animal
{
virtual void speak()
{
cout<<"Cat::Speak()"<<endl;
}
virtual void run()
{
cout<<"Cat::run()"<<endl;
}
~Cat()
{
cout<<"Cat::~Cat()"<<endl;
}
}
int main()
{
Animal *cat0=new Cat();
cat0->speak;
delete cat0;
}
纯虚函数
纯虚函数:没有函数体且初始化为0 的虚函数,用来定义接口规范(类似于Java中的抽象类接口)
struct Animal
{//动物的这些方法有实现是不合理的,所以将其设为纯虚函数
virtual void speak()=0;
virtual void run()=0;
}
struct dog:Animal
{
void speak()
{
cout<<"dog::speak()"<<endl;
}
void run()
{
cout<<"Animal::run()"<<endl;
}
}
struct cat:Animal
{
void speak()
{
cout<<"cat::speak()"<<endl;
}
void run()
{
cout<<"cat::run()"<<endl;
}
}
void liu(Animal *p)
{
p->speak();
p->run();
}
int main()
{
Animal *p=new cat();
p->speak();
p->run();
liu(new cat());
liu(new dog());
return 0;
}
抽象类(Abstract class)
含有纯虚函数的类,不可以实例化(不可以创建对象)
抽象类可以包含非纯虚函数、成员变量
如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类
多继承
C++允许一个类 可以有多个父类(不建议使用,会增加程序设计复杂度)
class Student
{
public:
int m_score;
void study()
{
cout<<"Student::study()"<<endl;
}
};
class Worker
{
public:
int m_salary;
void work()
{
cout<<"Worker::work()"<<endl;
}
};
class Undergraduate:public Student,public Worker
{
public:
int m_grade;
void play()
{
cout<<"Undergraduate::play()"<<endl;
}
};
多继承体系下的构造函数调用
struct Student
{
int m_score;
Student(int score):m_score(score){}
void study()
{
cout<<"Study::study()-score="<<m_score<<endl;
}
};
struct Worker
{
int m_salary;
Worker(int salary):m_salary(salary){}
void work()
{
cout<<"Work::work()-salary="<<m_salary<<endl;
}
};
struct Undergraduate:Student,Worker
{
int m_grade;
// Undergraduate(int score,int salary,int grade)
// :m_grade(grade),m_salary(salary),m_score(score){}
Undergraduate(int score,int salary,int grade)//多继承体系下的构造函数调用
:m_grade(grade),
Student(score),
Worker(salary){}
void play()
{
cout<<"Undergraduate::play()"<<m_grade<<m_salary<<m_score<<endl;
}
}
虚函数
如果子类继承的多个父类都有虚函数,那么子类就会产生对应的多张虚表
struct Student
{
virtual void study()
{
cout<<"Student::study()"<<endl;
}
};
struct Worker
{
virtual void work()
{
cout<<"Worker::work()"<<endl;
}
};
struct Undergraduate:Student,Worker
{
void study()
{
cout<<"Undergraduate::study()"<<endl;
}
void work()
{
cout<<"Undergraduate::work()"<<endl;
}
void play()
{
cout<<"Undergraduate::play()"<<endl;
}
}
同名函数
class Student
{
public:
void eat()
{
cout<<"Student::eat()"<<endl;
}
};
class Worker
{
public:
void eat()
{
cout<<"Worker::eat()"<<endl;
}
};
class Undergraduate:public Student,public Worker
{
public:
void eat()
{
cout<<"Undergraduate::eat()"<<endl;
}
};
int main()
{
Undergraduate ug;
ug.eat();
ug.Student::eat();//调用父类中的eat()
}
同名成员变量
class Student
{
public:
int m_age;
};
class Worker
{
public:
int m_age;
};
class Undergraduate:public Student,public Worker
{
public:
int m_age;
};
int main()
{
Undergraduate ug;
ug.m_age=10;
ug.Student::m_age=12;
return 0;
}
菱形继承
#include <iostream>
using namespace std;
struct Person
{
int m_age;
};
struct Student:Person
{
int m_score;
};
struct Worker:Person
{
int m_salary;
};
struct Undergraduate:Student,Worker
{
int m_grade;
};
int main()
{
Undergraduate ug;
cout<<sizeof(ug)<<endl;//结果为20
return 0;
}
菱形继承带来的问题:
最底下子类从基类继承的成员变量冗余、重复
最底下子类无法访问基类的成员,有二义性
虚继承
虚继承可以解决菱形继承带来的问题
class Person //Person 被称为虚基类
{
int m_age=1;
};
class Student:virtual public Person
{
int m_score=2;
};
class Worker:virtual public Person
{
int m_salary =3;
};
class Undergraduate:public Studetn,public Worker
{
int m_grade=4;
};
int main()
{
Undergraduate ug;
cout<<sizeof(ug)<<endl;
return 0;
}
多继承的应用
#include <iostream>
using namespace std;
class JobBaomu
{
public:
virtual void clean()=0;
virtual void cook()=0;
};
class JobTeacher
{
public:
virtual void playFootball()=0;
virtual void playBaseball()=0;
};
class Student:public JobBaomu,public JobTeacher
{
int m_score;
public:
void clean()
{
}
void cook()
{
}
};
class Worker
{
int m_salary;
};
int main()
{
/*
兼职中心,招聘兼职,岗位如下:
1.保姆:扫地、做饭
2.老师:踢足球、打棒球
应聘的角色
1.学生
2.上班族
*/
}
静态成员(static)
静态成员:被static修饰的成员变量、函数
可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态变量名)
静态成员变量
存储在数据段(全局区、类似于全局变量),整个程序运行过程中只有一份内存,不需要创建对象就存在
对比全局变量,它可以设定访问权限(public、protected、private),达到局部共享的目的
必须初始化,必须在类外面初始化,初始化时 不能带static,如果类的声明和实现分离(在实现.cpp中初始化)
静态成员函数
内部不能使用this指针(this指针只能用在非静态成员函数内部)
不能是虚函数(虚函数只能是非静态成员函数)PS:虚函数是用于多态,通过父类指针调用虚函数
内部不能访问非静态成员变量、函数,只能访问静态成员变量、函数。
非静态成员函数内部可以访问静态成员变量、函数。
静态成员函数之间可以相互访问。
构造函数和析构函数不能是静态的。
当声明和实现分离时,实现部分不能带static。
#include <iostream>
using namespace std;
class Car
{
private:
static int m_price;
public:
static void run()
{
cout<<"run()"<<endl;
}
};
int Car::m_price=0;//初始化
int main()
{
Car car1;
car1.m_price=100;
Car car2;
car2.m_price=200;
Car::m_price=400;
Car*p = new Car();
p->m_price=500;
//静态成员函数访问方式
Car.run();
Car->run();
Car::run();
return 0;
}
应用
#include <iostream>
using namespace std;
class Car
{
private:
static int ms_count; //统计创造Car对象的多少
public:
Car()
{
//严格来说,这里需要考虑多线程安全问题
ms_count++;
}
static int getCount() //可以不创建对象,通过类名直接访问,获得ms_count的值
{
return ms_count;
}
~Car()
{
ms_count--;
}
};
int Car::ms_count=0;
int main()
{
Car car;
Car *p = new Car();
return 0;
}
静态成员经典应用----单例模式
单例模式:设计模式的一种,保证某个类永远只创建一个对象
第一步:构造函数、析构函数私有化
第二步:定义一个私有的static成员变量指向唯一的那个单例对象
第三步:提供一个公共的访问单例对象的接口
#include <iostream>
using namespace std;
Class Rocket
{
private:
Rocket(){};//创建对象必须调用构造函数,一旦私有化就不能调用构造函数
~Rocket(){};
static Rocket *ms_rocket;//设置为静态是为了保证只有一个Rocket类
public:
static Rocket *sharedRocket()
{
//要考虑多线程安全
if(ms_rocket==NULL)
{
ms_rocket=new Rocket();
}
return ms_rocket;
}
static void deleteRocket()
{
//要考虑多线程安全
if(ms_rocket!=NULL)
{
delete ms_rocket;//将堆空间回收,但ms_rocket依然有值,不然有可能产生野指针。
ms_rocket=NULL;
}
}
void run()
{
cout<<"run()"<<endl;
}
};
int main()
{
Rocket *p1=Rocket::shareRocket();//通过调用静态函数来创建类
Rocket *p2=Rocket::shareRocket();
Rocket *p3=Rocket::shareRocket();
Rocket *p4=Rocket::shareRocket();
Rocket::deleteRocket();
cout<<p1<<endl;
cout<<p2<<endl;
cout<<p3<<endl;
cout<<p4<<endl;
return 0;
}
new、delete误区
int *p = new int;
*p = 10;
delete p; //回收堆空间4个字节,里面的内容还会在,不会被清除
回收堆空间内存:这块堆空间内存可以重新被别人使用
const成员
const成员:被const修饰的成员变量、非静态成员函数
const成员变量:
必须初始化(类内部初始化),可以在声明的时候直接初始化赋值。
非static的const成员变量还可以在初始化列表中初始化
const成员函数(非静态):
const关键字写在参数列表后面,函数的声明和实现必须带const
内部不能修改非静态成员变量
内部只能调用const成员函数、static成员函数
非const成员函数可以调用const成员函数
const成员函数和const成员函数构成重载
非const对象(指针)优先调用非const成员函数
const对象(指针)只能调用const成员函数、static成员函数
#include <iostream>
using namespce std;
class Car
{
public:
const int mc_price=0;//直接赋值
Car():m_price(0){} //初始化列表中初始化
void run() const
{
cout<<"run()"<<endl;
}
};
int main()
{
return 0;
}
引用类型成员
引用类型成员变量必须初始化(不考虑static情况)
在声明的时候直接初始化
通过初始化列表初始化
class Car
{
int age;
int &m_price = age;
public:
Car(int &price):m_price(price){}
};
拷贝构造函数(Copy Constructor)
拷贝构造函数是构造函数的一种
当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化
拷贝构造函数的格式是固定的,接收一个const引用作为参数
#include <iostream>
using namespace std;
class Car()
{
int m_price;
int m_length;
public:
Car(int price=0,int length=0):m_price(price),m_length(length)
{
cout<<"Car(int price=0,int length=0)"<<endl;
}
//拷贝构造函数
Car(const Car &car):m_price(car.m_price),m_length(car.m_length)
{
cout<<"Car(const Car &car)"<<endl;
}
void display()
{
cout<<"price="<<m_price<<",length="<<m_length<<endl;
}
};
int main()
{
Car car1;
Car car2(100);
Car car3(100,5);
//利用已经存在的car3对象创建了一个car4新对象
//car4初始化时会调用拷贝构造函数
Car car4(car3);
/*
具体拷贝过程
car4.m_price=car3.m_price;
car4.m_length=car4.m_length;
*/
getchar();
return 0;
}
如果没有拷贝构造函数,在默认情况下,也会将原先对象中所有的值拷贝到新的对象中
调用父类的拷贝构造函数
class Person
{
int m_age;
public:
Person(int age):m_age(age){}
Person(const Person &person):m_age(person.m_age){}
};
class Student:public Person
{
int m_score;
public:
Student(int age,int score):Person(age),m_score(score){}//调用父类的构造函数
Student(const Student &student):Person(student),m_score(student,m_score){}//调用父类的拷贝构造函数
}
int main()
{
Car car1(100,5);//调用 构造函数
Car car2(car1);//调用 拷贝构造函数
Car car3=car2;//等价于第二种写法 调用拷贝构造函数
Car car4;//调用 构造函数
car4=car3;//这里并不会调用拷贝构造函数,仅仅是简单的赋值操作
}
如果子类没有调用父类的拷贝构造函数会默认调用父类的构造函数,如果子类显式的调用了父类的拷贝构造函数,则不会调用父类的构造函数
深拷贝、浅拷贝
浅拷贝(shallow copy)
指针类型的变量只会拷贝地址值,地址拷贝
编译器默认的提供的拷贝是浅拷贝(shallow copy)
将一个对象中所有成员变量的值拷贝到另一个对象
如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
可能会导致堆空间多次free的问题
#include <iostream>
using namespace std;
class Car
{
int m_price;
char *m_name;
public:
Car(int price=0,char *name=NULL):m_price(price),m_name(name){}
void dispaly()
{
cout<<"price is"<<m_price<<",name is"<<m_name<<endl;
}
};
int main()
{
const char *name="bmw";
char name2[]={"b","m","w","\0"};//没写new就是在栈空间
/*char name2[]={"b","m","w","\0"};// "\0"标志着字符串已经结束,必须要写
cout<<strlen(name)<<endl;//求字符串长度,"\0"并不被计算,但存储时会将其一起存储
cout<<name2<<endl;
*/
Car *car=new Car(100,name2);//此时m_name存储的是name2的地址,堆空间m_name指向栈空间name2
/*堆空间指向栈空间是非常危险的,因为栈空间会被回收,那么堆空间所指向的位置就没有内容,此时堆空间就成为了空指针*/
return 0;
}
深拷贝(deep copy)
将指针指向的内容拷贝到新的存储空间
在浅拷贝的基础上,在堆空间继续申请空间,赋值原先堆空间中的内容,内容拷贝
如果要实现深拷贝(deep copy),就需要自定义拷贝构造函数
将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间
#include <iostream>
using namespace std;
class Car
{
int m_price;
char *m_name;
void copy(const char *name = NULL)
{
if(name==NULL) return;
//申请新的堆空间
m_name = new char[strlen(name)+1]{};//大括号会将申请堆空间数据清零
//拷贝字符串数据到新的堆空间
strcpy(m_name;name);
}
public:
Car(int price=0,const char *name=NULL):m_price(price)
{
/*if(name==NULL) return;
//申请新的堆空间
m_name = new char[strlen(name)+1]{};//大括号会将申请堆空间数据清零
//拷贝字符串数据到新的堆空间
strcpy(m_name;name);*/
copy(name);
}
Car(const Car &car):m_price(car.price)
{
/*if(car.m_name==NULL) return;
//申请新的堆空间
m_name=new char[strlen(name)+1]{};
//拷贝字符串数据到新的堆空间
strcpy(m_name,car.m_name);*/
copy(car.m_name);
}
void dispaly()
{
cout<<"price is"<<m_price<<",name is"<<m_name<<endl;
}
~Car()
{
if(m_name == NULL) return;
delete[] m_name;
m_name=NULL;
}
};
int main()
{
Car car1(100,"bmw");
Car car2=car1;//默认为浅拷贝
car2.display();
/*char name[]={'b','m','w','\0'};
Car *car = new Car(100,name);
car->display();
*/
return 0;
}
对象型参数和返回值
使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象
class Car
{
int m_price;
public:
Car(){}
Car(int price):m_price(price){}
Car(const Car &car):m_price(car.m_price){}
};
void test1(Car car)//当传入值时相当于 Car car = car1,相当于拷贝了一个对象,变为引用或指针即可 Car &car
{
}
Car test2()
{
Car car(20);//此处构造一个对象
return car;//相当于拷贝构造
};
int main()
{
Car car1(10);
test1(car1);
Car car2=test2();//默认构造一个对象,此种调用了一次拷贝构造(编译器做了优化,原来进行了两次),一次普通构造
Car car3(30);//构造一个函数,此种调用了一次拷贝构造,两次普通构造
car3 = test2();//并没有创建一个新对象,只是简单的复制
}
匿名对象(临时对象)
没有变量名、没有被指针指向的对象,用完后马上调用析构
class Car
{
};
int main()
{
Car();//匿名对象,创建结束后就会被回收
return 0;
}
隐式构造、explicit
隐式构造(转换构造)
C++中存在隐式构造的现象:某些情况下,会隐式调用单参数的构造函数
可以通过explicit 禁止隐式构造
class Person
{
int m_age;
public:
Person()
{
cout<<"Person()-"<<this<<endl;
}
explicit Person(int age):m_age(age)
{
cout<<"Person(int)-"<<this<<endl;
}
Person(const Person &person)
{
cout<<"Person(const Person &person)-"<<this<<endl;
}
~Person()
{
cout<<"~Person()-"<<this<<endl;
}
void display()
{
cout<<"display()-age is"<<this->m_age<<endl;
}
};
void test1(Person person=30)
{
}
Person test2()
{
return 40;
}
int main()
{
/*
Person p1;
Person p2(10);
Person p3=p2;
*/
Person p1=20;//调用单参数构造函数,Person(int age),等价于Person p1(20)
test1(30);
test2();
Person p1;//先构造一个对象
p1=40;//隐式构造一个对象
return 0;
}
编译器自动生成的构造函数
C++的编译器在某些特定的情况下,会给类自动生成无参的构造函数,比如
1.成员变量在声明的同时进行了初始化
2.有定义虚函数
3.虚继承其它类
4.包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
5.父类有构造函数(编译器生成或自定义)
总结:
对象创建时 ,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数
/*
很多教程都说:编译器会为每一个类都生成空的无参的构造函数 错误的
*/
class Person
{
public:
int m_price=5;//成员变量在声明的同时进行了初始化
//等价于
/*int m_price;
Person(int m_price=5){}*/
virtual void run(){}
}
int main()
{
Person person;
return 0;
}
友元
友元包括友元函数和友元类
???如果将函数A(非成员函数)声明为类C的友元函数,那么在函数A内部就能直接访问类C对象的所有成员
如果将类A声明为类C的友元类,那么在类A的所有成员函数内部都能直接访问类C对象的所有成员
class Point
{
friend Point add(Point,Point);//友元函数
friend class Math;//友元类
int m_x;
int m_y;
public:
int getX(){return m_x};
int getY(){return m_y};
Point(int x,int y):m_x(x),m_y(y){}
void display()
{
cout<<"("<<m_x<<","<<m_y<<")"<<endl;
}
};
Point add(Point p1,Point p2)
{
//return Point(p1.getX()+p2.getX(),p1.getY()+p2.getY());//调用过于频繁,浪费空间
return Point(p1.m_x+p2.m_x,p1.m_y+p2.m_y);//定义为友元函数之后就可以被允许直接访问私有成员变量
}
class Math
{
public:
void display(Point p1,Point p2)
{
cout<<p1.m_x<<","<<p2.m_y<<endl;
}
};
int main()
{
Point p1(10,20);
Point p2(20,30);
return 0;
}
内部类
如果将类A定义在类C的内部,那么类A就是一个内部类(嵌套类)
内部类的特点:
支持public、protected、private权限
成员函数可以直接访问其外部类对象的所有 成员(反过来则不行)
成员函数可以直接不带类名、对象名访问其外部类的static成员
不会影响外部类的内存布局
可以在外部类内部声明,在外部类外面进行定义
#include <iostream>
using namespace std;
class Person
{
static int ms_price;
int m_age;
public: //protected只有类内部以及子类能够使用,private只有类内部能够使用
class Car
{
int m_price;
ms_price=0;
void run()
{
cout<<m_age<<endl;
}
};
};
int main()
{
Person::Car car1;//定义Car对象,
return 0;
}
内部类-声明和实现分离
class Point
{
class Math
{
void test();
};
};
void Poinnt::Math::test()
{
}
class Point
{
class Math;
};
class Point::Math
{
voit test(){}
};
class Point
{
class Math;
};
class Point::Math
{
voit test();
};
voit Point::Math::test()
{
}
局部类
在一个函数内部定义的类,称为局部类
局部类的特点
作用域仅限于所在的函数内部
其所有成员必须定义在类内部,不允许定义static成员变量
成员函数不能直接访问函数的局部变量(static变量除外)
局部变量加上static修饰,其作用范围相当于全局变量
void test()
{
static int age=10;
//局部类
class Car
{
void run()
{
}
};
}
int main()
{
return 0;
}
运算符重载(operator overload)
运算符重载(操作符重载):可以为运算符增加一些新的功能
全局函数、成员函数都支持运算符重载
class Point
{
friend Point add(Point,Point);
friend void operator+(Point,Point);
friend ostream &operator<<(ostream &,const const Point &);
friend istream &operator>>(istream &,Point &);
int m_x;
int m_y;
public:
Point(int x,int y):m_x(x),m_y(y){}
void display()
{
cout<<"("<<m_x<<","<<m_y<<")"<<endl;
}
Point(const Point &point)
{
m_x=point.m_x;
m_y=point.m_y;
}
const void operator+(const Point &point) const//第一个const是用来限制返回值是个常量对象,不能给它赋值,第二个const是用来限制此函数为const函数,保证返回值能够再次调用此函数
{
return Point(this->m_x+point.m_x,this->m_y+point.m_y);
}
Point &operator+=(const Point &point)
{
m_x+=point.m_x;
m_y+=point.m_y;
return *this;
}
bool operator==(const Point &point)
{
return (m_x==point.m_x)&&(m_y==point.m_y);
}
const Point operator-() const
{
return Point(-m_x,-m_y);
}
Point &operator++()//前置
{
m_x++;
m_y++;
return *this;
}
Point operator++(int)//后置
{
Point old(m_x,m_y);
m_x++;
m_y++;
return old;
}
};
/*Point add(Point p1,Point p2)
{
return Point(p1.m_x+p2.m_x,p1.m_y+p2.m_y);
}*/
/*void operator+(const Point &p1,const Point &p2)
{
return Point(p1.m_x+p2.m_x,p1.m_y+p2.m_y);
}*/
//output stream -> ostream
ostream &operator<<(ostream &cout,const Point &point)
{
cout<<"("<<point.m_x<<","<<point.m_y<<")";
return cout;
}
//input stream -> istream
istream &operator>>(istream &cin,Point &point)
{
cin>>point.m_x;
cin>>point.m_y;
return cin;
}
int main()
{
Point p1(10,30);
Point p2(20,30);
Point p3(30,30);
//Point p3=add(p1,p2);
//
Point p3=p1+p2+p3;
p3.display();
p1+p2;//当重载写到类中时,相当于p1.operator+(p2);
p1+=p2;
(p1+=p2)=Point(40,50);
-p1;
Point p3=-(-p1);
cout<<p1;//等价于operator(cout,p1)
cin>>p1;
cin>>p1>>p2;
return 0;
}
调用父类的运算符重载函数
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
Person &operator=(const Person &person)
{
m_age=person.m_age;
}
};
class Student:public Person
{
public:
int m_score;
Student &operator=(const Student &student)
{
Person::operator=(student);//调用父类的运算符重载函数
m_score=student.m_score;
}
}
int main()
{
Student stu1;
stu1.m_age=20;
stu1.m_score=100;
return 0;
}
仿函数
将一个对象当作一个函数一样来使用
对比普通函数,它作为对象可以保存状态;
#include <iostream>
using namespace std;
class Sum
{
int m_age;
public:
int operator()(int a,int b)
{
return a+b;
}
void func()
{
}
};
int main()
{
Sum sum;
cout<<sum(10,20)<<endl;
return 0;
}
运算符重载注意点
有些运算符重载不可以被重载,比如
对象成员访问运算符: .
域运算符:::
三目运算符:?:
sizeof
有些运算符只能重载为成员函数,比如
赋值运算符:=
下标运算符:[]
函数运算符:()
指针访问运算符:->
模板(template)
泛型,是一种将类型参数化以达到代码复用的技术,C++中使用模板来实现泛型
模板的使用格式
template <typename\class T>
typename和class是等价的
模板没有被使用时,是不会被实例化出来的
#include <iostream>
using namespace std;
class Point
{
friend ostream& operator<<(ostream& , const Point&);
int m_x;
int m_y;
public:
Point(int x, int y) :m_x(x), m_y(y) {}
Point operator+(const Point& point)
{
return Point(m_x + point.m_x, m_y + point.m_y);
}
};
ostream& operator<<(ostream& cout, const Point& point)
{
return cout << "(" << point.m_x << "," << point.m_y << ")";
}
//int add(int a, int b)
//{
// return a + b;
//}
//
//double add(double a, double b)
//{
// return a + b;
//}
//
//Point add(Point a, Point b)
//{
// return a + b;
//}
template <typename T> T add(T a, T b)//泛型
{
return a + b;
}
int main()
{
add<int>(10, 20);
/*cout << add(10, 20) << endl;
cout << add(1.5, 1.6) << endl;*/
return 0;
}
模板的声明和实现如果分离到.h和.cpp中,会导致链接错误
一般将模板的声明和实现统一放到一个.hpp文件中
参数模板
template<class T>
void swapValues(T &v1,T &v2)
{
T temp = v1;
v1 = v2;
v2 = temp;
}
int main()
{
int a=10;
int b=20;
swapValues<int>(a,b);
swapValues(a,b);
}
多参数模板
template<class T1,class T2>
void display(const T1 &v1,const T2 &v2)
{
cout<<v1<<endl;
cout<<v2<<endl;
}
动态数组、类模板
具体内容请见 实验 类模板
#include <iostream>
using namespace std;
class Point
{
int m_x;
int m_y;
public:
Point(int x, int y) :m_x(x), m_y(y) {}
};
template<typename Element>
class Array
{
//用于指向首元素
Element* m_data;
//元素个数
int m_size;
//容量
int m_capacity;
public:
Array(int capacity=0)
{
m_capacity = (capacity > 0) ? capacity : 10;
//申请堆空间
m_data = new Element[m_capacity];
}
~Array()
{
if (m_data == NULL) return;
delete[] m_data;
}
void add(Element value)
{
if (m_size == m_capacity)
{
//扩容
/*
* 1.申请一块更大的存储空间
* 2.将就空间的数据拷贝到新空间
* 3.释放旧空间
*/
cout << "空间不够" << endl;
return;
}
m_data[m_size++] = value;
}
Element get(int index)
{
if (index < 0 || index >= m_size)
{
//报错,抛出异常
throw "数组下标越界";
}
return m_data[index];
}
int size()
{
return m_size;
}
Element operator[](int index)
{
return get(index);
}
};
int main()
{
Array<int> array(3);
array.add(10);
array.add(20);
array.add(30);
array.add(40);
array.add(50);
cout << array.get(0) << endl;
cout << array[1] << endl;
cout << array.size() << endl;
Array<Point> array1(2);
Point point(1, 2);
array1.add(Point(1, 2));
array1.add(Point(3, 4));
return 0;
}
类型转换
c语言风格的类型转换符
(type)expression
type(expression)
int a=10;
double d=(double) a;
double d2=double(a);
C++中有4个类型转换符
static_cast
dynamic_cast
reinterpret_cast
const_cast
使用格式:XX_cast(expression)
int a = 10;
double d = static_cast<double>(a);
const_cast
一般用于去除const属性,将const转换成非const
const Person *p1 = new Person();
p1->m_age = 10;
Person *p2 = const_cast<Person *>(p1); //将const转换为非const,p1和p2的值相同
Person *p3=(Person *)p1; //与上面没区别,这是c语言的写法
p2->m_age = 20;
dynamic_cast
一般用于多态类型的转换,有运行时安全检测,不安全时将指针赋值为空指针
class Person
{
virtual void run(){}
};
class Student:public Person{};
class Car{};
int main()
{
Person *p1 = new Person();
Person *p2 = new Student();
Student *stu1 = (Student *)p1;//不安全,子类指针指向父类,不会进行安全检测
Student *stu2 = dynamic_cast<Studnet *>(p2);//安全
Car *c1 = (Car*)p1;//没有检测
Car *c2 = dynamic_cast<Car *>(p2);//不安全,没有赋值
return 0;
}
static_cast
对比dynamic_cast,缺乏运行时安全检测
不能交叉转换(不是同一继承体系的,无法转换)
交叉转换:没有任何联系的两个类之间进行的转换
常用于基本数据类型的转换、非const转成const
reinterpret_cast
属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝
Person *p1 = new Person();
Person *p2 = new Student();
Student *stu1 = reinterpret_cast<Student *>(p1);
Student *stu2 = reinterpret_cast<Student *>(p2);
Car *car = reinterpret_cast<Car *>(p1);
int *p = reinterpret_cast<int *>(100);
int num = reinterpret_cast<int>(p);
int i = 10;
double d1 = reinterpret_cast<double &>(i);//结果并不相等
C++11新特性
auto
可以从初始化表达式中推断出变量的类型,大大简化编程工作
属于编译器特性,不影响最终的机器码质量,不影响运行效率
auto i = 10;//int
auto p = new Person
decltype
可以获取变量类型
int a = 10;
decltype(a) b = 20;//int
nullptr
可以解决NULL二义性的问题
int *p1 = nullptr;//空指针
快速遍历
int array[]={1,2,3,4};
int array[]{1,2,3,4};//更简洁的初始化数组方式
for(int item:array)
{
cout<<item<<endl;
}
Lambda表达式
有点类似于JavaScript中的闭包、iOS中的Block,本质就是函数
完整结构:
[capture list](params list)mutable exception->return type{function body}
capture list:捕获外部变量列表
params list:形参列表,不能使用默认 参数,不能省略参数名
mutable:用来使用是否可以修改捕获的变量
exception:异常设定
return type:返回值类型
function body:函数体
有时可以省略部分结构
[capture list](params list)->return type{function body}
[capture list](params list){function body}
[capture list]{function body}
int add(int v1,int v2)
{
return v1 + v2;
}
int sub(int v1,int v2)
{
return v1 - v2;
}
int multiple(int v1,int v2)
{
return v1 * v2;
}
int divide(int v1,int v2)
{
return v1 / v2;
}
int exec(int v1,int v2,int (*func)(int ,int ))
{
return func(v1,v2);
}
int main()
{
///
cout<<exec(10,20,add)<<endl;
cout<<exec(10,20,sub)<<endl;
cout<<exec(10,20,multiple)<<endl;
cout<<exec(10,20,divide)<<endl;
//等价于
cout<<exec(20,10,[](int v1,int v2){return v1+v2;})<<endl;
cout<<exec(20,10,[](int v1,int v2){return v1-v2;})<<endl;
cout<<exec(20,10,[](int v1,int v2){return v1*v2;})<<endl;
cout<<exec(20,10,[](int v1,int v2){return v1/v2;})<<endl;
///
([] //最简单的lambda表达式
{
cout<<"func"<<endl;
})(); //调用lambda表达式
///
void (*P)()=[]
{
cout<<"func"<<endl;
} //存储lambda表达式
auto p=[]
{
cout<<"func"<<endl;//等价于上面
}
p();
p();
///
auto p = [](int a,int b)->int//带有返回值的lambda表达式
{
return a+b;
}
cout<<p(10,20)<<endl;
///
return 0;
}
变量捕获
int main()
{
int a=10;
//默认都是值捕获
auto func = [a] //变量捕获
{
cout<<a<<endl;
};
//地址捕获
auto func = [&a] //变量捕获
{
cout<<a<<endl;
};
//隐式捕获(值捕获)
auto func = [=]
{
cout<<a<<endl;
};
//隐式捕获(地址捕获)
auto func = [&]
{
cout<<a<<endl;
}
a=20;
func();
return 0;
}
mutable
int a=10;
/*auto func = [&a]
{
a++;
};*/
//等价于上面
auto func = [a]()mutable
{
a++;//11
};
func()
cout<<a<<endl;//10
return 0;
C++14
泛型Lambda表达式
auto func = [](auto v1,auto v2){return v1+v1;};
cout<<func(10,20.5)<<endl;
对捕获的变量进行初始化
int a;
auto func = [a = 10]()
{
cout<<a<<endl;
};
func();
cout<<a<<endl;
C++17
可以进行初始化的if、switch语句
//变量a,b的作用域使它所在的if语句、以及其后面的if-else语句
if(int a=10;a>10)
{
a=1;
}
else if(int b=20;a>5&&b>10)
{
b=2;
a=2;
}
else if(0)
{
b=3;
a=3;
}
else
{
b=4;
a=4;
}
//变量a的作用域是它所在的switch语句
switch(int a=10,a)
{
case 1:
break;
case 2:
break;
default:
break;
}
异常
编程过程中的常见错误类型
语法错误
逻辑错误
异常
异常时一种在程序运行过程中可能会发生的错误(比如内存不够)
异常没有被处理,会导致程序终止
int main()
{
cout<<1<<endl;
for(int i=0;i<9999;i++)
{
//这句代码可能会产生异常(抛出异常)、系统抛出异常
try
{
int *p=new int[999999];
}
catch()
{
cout<<"产生异常:内存不够用";
break;
}
}
}
抛出异常
throw异常后,会在当前函数中查找匹配的catch,找不到就终止当前函数代码,去上一层函数中查找。如果最终都找不到匹配的catch,整个程序就会终止。
int divide(int v1,int v2)
{
if(v2==0)
{
throw "不能除以0"
}
}
int main()c
{
int a=10;
int b=0;
try
{
cout<<divide(a,b)<<endl;
}
catch(const char * exception)
{
cout<<"产生异常"<<exception<<endl;
}
catch(int exception)
{
cout<<"产生异常"<<exception<<endl;
}
}
异常的抛出声明
为了增强可读性和方便团队协作,如果函数内部可能会抛出异常,建议函数声明一下异常类型
//抛出任意可能的异常
void func1()
{
}
//不抛出任何异常
void func2() throw()
{
}
//只抛出int、double类型的异常
void func3() throw(int,double)
{
}
自定义异常类型
//所有异常的基类
class Exception
{
public:
virtual const char *what() const=0;
};
class DivideException:public Exception
{
public:
const char *what() const
{
return "不能除以0";
}
};
int divide(int v1,int v2)
{
if(v2==0)
{
//抛出异常
throw DivideException();
}
return v1/v2;
}
void test()
{
try
{
int a = 10;
int b = 0;
cout<<divide(a,b)<<endl;
}
catch(const DivideException &exception)
{
cout<<"产生了异常(DivideException)"<<exceptioin.what()<<endl;
}
}
拦截所有类型的异常
try
{
int a=10;
int b=0;
int c = dibide(a,b);
}
catch(...)//拦截所有类型的异常
{
cout<<"出现异常"<<endl;
}
标准异常(std)
系统自带的异常
智能指针(Smart Pointer)
传统指针存在的问题
需要手动管理内存
容易发生内存泄漏(忘记释放、出现异常等)
释放之后产生野指针
智能指针就是为了解决传统指针存在的问题
auto_ptr:属于C++98标准,在C++11中已经不推荐使用(有缺陷,不如不能用于数组)
shared_ptr:属于C++11标准
unique_ptr:属于C++11标准
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
Person()
{
cout << "Person()" << endl;
}
Person(int age):m_age(age){}
~Person()
{
cout << "~Person()" << endl;
}
void run()
{
cout << "run()-" << m_age << endl;
}
};
void test()
{
//Person* p = new Person(20);
//可以理解为:智能指针p指向了堆空间的Person对象
auto_ptr<Person> p(new Person(20));
p->run();
}
int main()
{
test();
{//会报错,auto_ptr不能指向数组
auto_ptr<Person> p(new Person[10]);
p->run()
}
{
shared_ptr<Person[]> p(new Person[30]);//share_ptr 这样使用
cout<<p1.use_count()<<endl;//查看强引用计数
shared_ptr<Person[]> p2=p;
shared_ptr<Person[]> p3=p2;
p->run();
}
return 0;
}
智能指针的简单自实现
template<class T>
class SmartPointer
{
T *m_pointer;
public:
SmartPointer(T* pointer):m_pointer(pointer){}
~SmartPointer()
{
if(m_pointer == nullptr) return;
delete m_pointer;
}
T *operator->()
{
return m_pointer;
}
};
智能指针就是对传统指针的再度封装
shared_ptr
shared_ptr的设计理念
多个shared_ptr可以指向同一个对象,当最后一个shared_ptr在作用域范围内结束时,对象才会释放
可以通过一个已存在的智能指针初始化一个新的智能指针
shared_ptr<Person> p1(new Person());
shared_ptr<Person> p2(p1);
针对数组的用法
shared_ptr<Person> ptr1(new Person[5]{},[](Person *p){delete[] p;})
原理
一个shared_ptr会对一个对象产生强引用(strong reference)
每个对象都有个与之对应的强引用计数,记录着当前对象被多少个shared_ptr强引用着
当有一个新的shared_ptr指向对象时,对象的强引用计数就会+1
当有一个shared_ptr销毁时(比如作用域结束),强引用计数就会-1
当一个对象的强引用计数为0时(没有任何shared_ptr指向对象时),对象就会自动销毁(析构函数)
shared_ptr的循环引用
只针对智能指针
weak_ptr 会对一个对象产生弱引用
weak_ptr 可以指向对象解决shared_ptr的循环引用问题
class Person;
class Car
{
public:
shared_ptr<Person> m_person = nullptr;//将其变为weak_ptr<Person> m_person = nullptr,不会使强引用加1
Car()
{
cout<<"Car()"<<endl;
}
~Car()
{
cout<<"~Car()"<<endl;
}
};
class Person
{
public:
shared_ptr<Car> m_car = nullptr;
Person()
{
cout<<"Person()"<<endl;
}
~Person()
{
cout<<"~Person()"<<endl;
}
};
int main()
{
{//会导致内存泄漏
shared_ptr<Person> Person(new Person());
shared_ptr<Car> car(new Car());
person->m_car = car;
car->m_person = person;
}
return 0;
}
unique_ptr
unique_ptr也会对一个对象产生强引用,它可以确保统一时间只有一个指针指向对象
当unique_ptr销毁时(作用域结束时),其指向的对象也就自动销毁了
可以使用std::move函数转移unique_ptr的所有权
class Person;
class Car
{
public:
shared_ptr<Person> m_person = nullptr;
Car()
{
cout<<"Car()"<<endl;
}
~Car()
{
cout<<"~Car()"<<endl;
}
};
class Person
{
public:
shared_ptr<Car> m_car = nullptr;
Person()
{
cout<<"Person()"<<endl;
}
~Person()
{
cout<<"~Person()"<<endl;
}
};
int main()
{
//ptr1强引用着Person对象
unique_ptr<Person> ptr1(new Person());
//转移之后,ptr2强引用着person对象
unique_ptr<Person> ptr2 = std::move(ptr1);
return 0;
}