一、基础入门
注意:我们在定义变量、结构体时(如 int a=10),其实我们在代码区保存了a的地址(&a),当代码运行到的时候,就会找到a的值。
而我们在定义指针、数组时(如 int*p=&a),我们在代码区保存了p的地址,而在p的内容则是a的地址,当代码运行到的时候,其实将通过代码区p的地址找到p的值(就是&a)。
总结一下就是代码区保存各种变量的地址,无论是普通变量、结构体、数组还是指针。但变量和结构体在内存保存的是值不是地址,而数组和指针则是保存的值是32位的地址。
int a=10;代码区保存了&a
int*p=& a;代码区保存了&p
os在运行代码区的代码时。通过&a、&p找到全局变量区的a和p,而p的内容则是a的地址&a,我们可以通过*p表示a的内容。
1.基础知识
对于未初始化的全局变量一般初始化为0;但对于未初始化的指针则是随意赋值的;对于new出来的未初始化的堆区也是0,但对于malloc出来的堆区则是随意赋值的。
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
++前置递增 a=2; b=++a; 结果:a=3; b=3;
++后置递增 a=2; b=a++; 结果:a=3; b=2;
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
利用sizeof关键字可以统计数据和类型所占内存字节数
int a=10;
sizeof(a);
sizeof(int);
数据类型**** 占用空间** 取值范围s
short(短整型) 2字节 (-2^15 ~ 2^15-1)
int(整型) 4字节 (-2^31 ~ 2^31-1)
long(长整形) Windows为4字节,Linux为4字节(32位),8字节(64位) (-2^31 ~ 2^31-1)
long long(长长整形) 8字节 (-2^63 ~ 2^63-1)
float 4字节 7位有效数字
double 8字节 15~16位有效数字
_____________________________________________________________________________________________________
do...while循环语句
与while的区别在于:do...while会先执行一次循环语句==,再判断循环条件
int main() {
int num = 0;
do
{
cout << num << endl;
num++;
} while (num < 10);
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
goto语句:无条件跳转语句
int main() {
cout << "1" << endl;
goto FLAG;
cout << "2" << endl;
cout << "3" << endl;
cout << "4" << endl;
FLAG:
cout << "5" << endl;
system("pause");
return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————
&按位与
&&与
2.数组和指针
数组和指针没有本质区别的:p和a都指向数组的首地址。但数组作为参数传入函数时,往往代表数组本身,而指针代表数组的首地址。
但指针在作为sizeof的参数时,返回的是数组的字节数,而指针则表示的是指针本身的长度。
int a[]={};
int*p=a;
cout<<sizeof(a)<<sizeof(p);
cout<<a<<p;
数组可以用下面的方法遍历,而指针不行
for (int i:a)
{cout<<i;}
_______________________________________________________________________________________________________________________
C++独有的数组遍历方式
int main(int argc, char **argv)
{
int a[]={1,2,3,4,5,6,7,8,9};
for (int i : a )
cout<<i<<endl
不能把数组地址传给指针,在由指针通过这种方式遍历。
// int*p=a; //指针必须通过数组得到数组。
//for (int i : p )
// cout<<i<<endl;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
二维数组实际上是一个一重指针不是双重指针,a[3][2],a指向数组首地址,但a【0】指向第一行的首地址,这是编译器设置的。我们用数值指针指向一个二维数组。
int a[3][2] = { 1,2,3,4,4,5 };
int(* p)[2] = a;
二维数组只能用这种方式的指针接收二维数组。不能用*P和*PP接收一个二维数组。
我们分析一下int(* p)[2],(*P)作为(*P)[2]的一维数组标号自然指向这个一维数组的首地址,这个一维数组的每个元素都是
数组(a的前三个元素组成的数组和后三个元素组成的数组),而p又是指向的a前三个组成的数组,所以p就指向了a,也就是数组的首地址。
总结一下:我们的(*p)同时又是一个指针类型。当int(* p)[2]指向一个二维数组int a[3][2],*p就是a【0】,也就是第一行的首地址,p就是a,也就是整个数组的首地址。
p==a;
*p==a[0];
**p==a[0][0];
对于我们的数组指针int*pp[3],其实是一个一维数组,数组里存放的是地址。所以我们可以用二维指针去指向
int**p=pp;
一维数组定义的三种方式:
1. 数据类型 数组名[ 数组长度 ];
2. 数据类型 数组名[ 数组长度 ] = { 值1,值2 ...};
3. 数据类型 数组名[ ] = { 值1,值2 ...};
二维数组定义的四种方式:
5. ` 数据类型 数组名[ 行数 ][ 列数 ]; `
6. `数据类型 数组名[ 行数 ][ 列数 ] = { {数据1,数据2 } ,{数据3,数据4 } };`
7. `数据类型 数组名[ 行数 ][ 列数 ] = { 数据1,数据2,数据3,数据4};`
8. ` 数据类型 数组名[ ][ 列数 ] = { 数据1,数据2,数据3,数据4};
int main() {
//二维数组数组名
int arr[2][3] =
{
{1,2,3},
{4,5,6}
};
cout << "二维数组大小: " << sizeof(arr) << endl;
cout << "二维数组一行大小: " << sizeof(arr[0]) << endl;
cout << "二维数组元素大小: " << sizeof(arr[0][0]) << endl;
cout << "二维数组行数: " << sizeof(arr) / sizeof(arr[0]) << endl;
cout << "二维数组列数: " << sizeof(arr[0]) / sizeof(arr[0][0]) << endl;
//地址
cout << "二维数组首地址:" << arr << endl;
cout << "二维数组第一行地址:" << arr[0] << endl;
cout << "二维数组第二行地址:" << arr[1] << endl;
cout << "二维数组第一个元素地址:" << &arr[0][0] << endl;
cout << "二维数组第二个元素地址:" << &arr[0][1] << endl;
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
函数分文件编写一般有4个步骤
1. 创建后缀名为.h的头文件
2. 创建后缀名为.cpp的源文件
3. 在头文件中写函数的声明
4. 在源文件中写函数的定义
//swap.h文件
#include<iostream>
using namespace std;
//实现两个数字交换的函数声明
void swap(int a, int b);
//swap.cpp文件
#include "swap.h"
void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
//main函数文件
#include "swap.h"
int main() {
int a = 100;
int b = 200;
swap(a, b);
system("pause");
return 0;
}
-————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
int main() {
int a = 10;
int * p;
p = &a; //指针指向数据a的地址
cout << *p << endl; //* 解引用
cout << sizeof(p) << endl;
cout << sizeof(char *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(double *) << endl;
system("pause");
return 0;
}
所有指针类型在32位操作系统下是4个字节
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
指针常量和常量指针都是需要初始化的。
const修饰指针:看const右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量
int main() {
int a = 10;
int b = 10;
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
//const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误
system("pause");
return 0;
}
值传递和地址传递,函数调用时将实参中的值传给形参。当将地址作为参数传给形参时,形参也会指向实参指向的内存位置。
//值传递
void swap1(int a ,int b)
{
int temp = a;
a = b;
b = temp;
}
//地址传递
void swap2(int * p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main() {
int a = 10;
int b = 20;
swap1(a, b); // 值传递不会改变实参
swap2(&a, &b); //地址传递会改变实参
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
3.结构体
语法:struct 结构体名 { 结构体成员列表 };`
通过结构体创建变量的方式有三种:
- struct 结构体名 变量名
- struct 结构体名 变量名 = { 成员1值 , 成员2值...}
- 定义结构体时顺便创建变量
//结构体定义
struct student
{
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
}stu3; //结构体变量创建方式3
int main() {
//结构体变量创建方式1
struct student stu1; //struct 关键字可以省略
stu1.name = "张三";
stu1.age = 18;
stu1.score = 100;
cout << "姓名:" << stu1.name << " 年龄:" << stu1.age << " 分数:" << stu1.score << endl;
//结构体变量创建方式2
struct student stu2 = { "李四",19,60 };
cout << "姓名:" << stu2.name << " 年龄:" << stu2.age << " 分数:" << stu2.score << endl;
stu3.name = "王五";
stu3.age = 18;
stu3.score = 80;
//结构体数组
struct student arr[3]=
{
{"张三",18,80 },
{"李四",19,60 },
{"王五",20,70 }
};
cout << "姓名:" << stu3.name << " 年龄:" << stu3.age << " 分数:" << stu3.score << endl;
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
数组符号表示数组的首地址
通过改变指针的选择指向数组的某个值
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int * p = arr; //指向数组的指针
* (p+1);
*
结构体符号表示结构体本身
通过指定p的参数指定对应的值。
struct student stu = { "张三",18,100, };
struct student * p = &stu;
p->name ==*p.name;
二、核心编程
1.malloc/delete和new/free
我们若想在函数内定义一个数组a[n],数组的长度n又是一个变量(传参)时,我们不能定义一个普通的数组,因为即使在函数内数组的长度
也必须是常数,所以我们就得定义一个堆区的数组,堆区是动态增长的所以可以长度是可以是一个变量。int*a=new int[n];
void *malloc(int size)和void free(void *FirstByte)
malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。
malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。
int* p;
p = (int *) malloc (sizeof(int)*128);//分配128个(可根据实际需要替换该数值)整型存储单元,并将这128个连续的整型存储单元的首地址存储到指针变量p中
free(p)
double *pd=(double *) malloc (sizeof(double)*12);//分配12个double型存储单元,并将首地址存储到指针变量pd中
free(pd)
new 返回指定类型的指针,并且可以自动计算所需要大小。
new 可以指定分配堆区的值,没有指定默认初始化0.
nt *p;
p = new int; //返回类型为int* 类型(整数型指针),分配大小为 sizeof(int); 初始化为0
delete p ;// 释放单个对象
int *pi=new int(100); //指针pi所指向的对象初始化为100
delete pi ;// 释放单个对象
int* parr;
parr = new int [100]; //返回类型为 int* 类型(整数型指针),分配大小为 sizeof(int) * 100;
delete [ ]parr;//释放数组
new/delete和 malloc/free最大的不同在于:new/delete 对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。但malloc/free是库函数而不是运算符,
不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
c++对于非内部的数据类型,需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
对于内部数据而言,不需要调用构造和析构函数,因此new/delete malloc/free都是可以用的。但是对于非内部数据,需要使用new/delete。
2.空指针和野指针
int*p=Null;
int*pi;
p初始化为Null,其实Null是指向本进程某个段的段首地址。
pi未初始化其值,系统会为其随意赋值,因此可能会指向一个一个不可以访问的内存区域。
造成野指针的几个情况:
1.指针未初始化
2.delete和free后,指向堆区的指针任然指向原来的地方,变成了野指针。
3.全局指针指向了某个函数内部的变量,函数释放变量后,全局指针变成了野指针。
3.引用
引用常量指针必须初始化,引用内部实现其实就是一个int* const p;
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int &c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用```
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
值传递不会改变实参的值,而地址和引用传递会改变实参的值,这是因为引用将形参的指向实参,引用就像是变量的别名。
//1. 值传递
void mySwap01(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//2. 地址传递
void mySwap02(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//3. 引用传递
void mySwap03(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
mySwap01(a, b);
cout << "a:" << a << " b:" << b << endl;
mySwap02(&a, &b);
cout << "a:" << a << " b:" << b << endl;
mySwap03(a, b);
cout << "a:" << a << " b:" << b << endl;
system("pause");
return 0;
}
_____________________________________________________________________________________________________________________________
引用局部变量,函数结束局部变量释放,引用的值也随之释放,因为引用本质上就是一个指针常量。
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
//返回静态变量引用
int& test02() {
static int a = 20;
return a;
}
int main() {
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
system("pause");
return 0;
}
4.函数进阶
占位符
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "this is func" << endl;
}
int main() {
func(10,10); //占位参数必须填补
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
函数重载时有无const的区别,当常数和const修饰的常变量作为实参传给新参时,有const重载的函数则调用有const重载的函数。
//1、引用作为重载条件
void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}
//2、函数重载碰到函数默认参数
void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}
void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}
int main() {
int a = 10;
func(a); //调用无const
func(10);//调用有const
//func2(10); //碰到默认参数产生歧义,需要避免
system("pause");
return 0;
}
5.类和对象
//三种权限
//公共权限 public 类内可以访问 类外可以访问
//保护权限 protected 类内可以访问 类外不可以访问
//私有权限 private 类内可以访问 类外不可以访问
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
struct和class唯一区别在于默认权限不同
struct默认是公共
class默认是私有
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
编译器默认添加了一个无参构造函数和一个无参析构函数以及一个拷贝构造函数。
实例化常见方法:
Person p;
Person p1(10);
Person p4 = 10; // Person p4 = Person(10);
Person p2 = Person(10);
Person p3 = Person(p2);
Person p5 = p4; // Person p5 = Person(p4);
Person p6(p4);
Person(10)、Person()、Person(p1)单独写就是匿名对象 当前行结束之后,马上析构
Person p()错误,编译器会以为是函数声明
//1、构造函数分类
// 按照参数分类分为 有参和无参构造 无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};
//2、构造函数的调用
//调用无参构造函数
void test01() {
Person p; //调用无参构造函数
}
//调用有参的构造函数
void test02() {
//2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();
//2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构
//2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//Person p5(p4);
}
int main() {
test01();
//test02();
system("pause");
return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
深拷贝和浅拷贝
我们在调用拷贝构造创建的新对象时,可能会将新对象里的某个指针指向旧对象指针指向的的同一个堆区,旧对象和新对象调用析构时,会报错。应该在拷贝构造的时候不能是,指针传指针,而是new一个新的堆区。
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int age ,int height) {
cout << "有参构造函数!" << endl;
m_age = age;
m_height = new int(height);
}
//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
public:
int m_age;
int* m_height;
};
void test01()
{
Person p1(18, 180);
Person p2(p1);
cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}
int main() {
test01();
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————
初始化列表
构造函数():属性1(值1),属性2(值2)... {}
class Person {
public:
传统方式初始化
//Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
//}
//初始化列表方式初始化
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
void PrintPerson() {
cout << "mA:" << m_A << endl;
cout << "mB:" << m_B << endl;
cout << "mC:" << m_C << endl;
}
private:
int m_A;
int m_B;
int m_C;
};
int main() {
Person p(1, 2, 3);
p.PrintPerson();
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
静态成员
- 静态成员变量
- 所有对象共享同一份数据
- 类内声明,类外初始化,并在初始化的时候分配空间(未初始化的静态成员变量不能使用)
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量和静态成员函数
静态成员函数没有this指针,保存在全局变量区,因而无法调用非静态成员。(非静态成员在实例化后,其地址各不相同,没有this指针就无法找到实例化的成员)
class Person
{
public:
//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量
static void func()
{
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; //错误,不可以访问非静态成员变量
}
static int m_A; //静态成员变量
int m_B; //
private:
//静态成员函数也是有访问权限的
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_A = 10;
void test01()
{
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.func();
//2、通过类名
Person::func();
//Person::func2(); //私有权限访问不到
}
int main() {
test01();
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————------------——————————————————————
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
每个实例化的对象都有各自的成员变量,但成员函数是共有的,对象里只能找到调用函数的地址。
空指针可以调用没有使用成员变量的函数的原因是,Person * p = NULL,指针虽然指向NULL,但在NULL此处生成了一个person地址结构的区域,该区域也保存了成员函数的地址,但其函数对象地址是固定的,成员函数是存在于对象之外的,对象只能保存了成员函数的地址。
//空指针访问成员函数
class Person {
public:
void ShowClassName() {
cout << "我是Person类!" << endl;
}
void ShowPerson() {
if (this == NULL) {
return;
}
cout << mAge << endl;
}
public:
int mAge;
};
void test01()
{
Person * p = NULL;
p->ShowClassName(); //空指针,可以调用成员函数
p->ShowPerson(); //但是如果成员函数中用到了this指针,就不可以了
}
int main() {
test01();
system("pause");
return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
const修饰函数和对象,要求不能更改未加关键字mutable变量的值和调用不是常成员函数的函数。
**常函数:**
- 成员函数后加const后我们称为这个函数为**常函数**
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
-
**常对象:**
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}
//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {
//const Type* const pointer;
//this = NULL; //不能修改指针的指向 Person* const this;
//this->mA = 100; //但是this指针指向的对象的数据是可以修改的
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}
void MyFunc() const {
//mA = 10000;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量
//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数
}
int main() {
test01();
system("pause");
return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
友元,全局函数,类,成员函数可以做友元,友元可以访问public和private成员。
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building)
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
运算符重载
class MyInteger {
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() {
m_Num = 0;
}
//前置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}
//后置++
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
private:
int m_Num;
};
ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}
//前置++ 先++ 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}
//后置++ 先返回 再++
void test02() {
MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}
int main() {
test01();
//test02();
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
public继承可以访问public和protected
protected继承可以访问protected和public
private继承可以访问private和public
子类无论那种方式继承父类,都继承了父类的所有成员变量,继承方式会将对应的某些成员变量隐藏起来。
子类不仅继承了父类的成员变量,也继承了父类的成员函数,和静态成员函数和静态成员变量。但是函数和今天成员是所有对象共有的,父子类中只是记录了其地址,所有实例化的对象都只是通过地址访问。
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
子类和父类有相同的成员函数名或成员变量名时,默认子类的对象调用子类本身的成员,而必须通过指定作用域来调用父类成员。
父子类都有变量m_A和函数fun();
s.func();
s.Base::func();
s.m_A
s.Base::m_A
父子类同名静态成员函数和静态成员变量时,除了非静态成员函数和变量的方法,还可以直接通过类名调用。
因为静态成员是共有的,非静态成员变量随着实例化而复制一份,每个成员变量都有其this指向的偏移地址,所以不能通过类名调用,
Son s;//实例化子类s
s.func();
s.Base::func();
Son::func();//通过类名调用
Son::Base::func();
s.m_A
s.Base::m_A
Son::m_A
Son::Base::m_A
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
菱形继承
虚继承,当虚继承发生时,编译器会拷贝一份父类的数据,虚继承的类会通过指针维护这份数据,当孙类继承一个或多个虚继承的父类,孙类会继承父类维护的从爷类拷贝的数据。
class Animal
{
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
当子类重写父类中的虚函数,并且父类指针或引用指向子类对象时,父类指针调用虚函数是子类重写的虚函数,而不是本身的虚函数。
当类中存在纯虚函数时,该类就是无法实例化对象的抽象类,子类必须重写抽象类中的纯虚函数,否则也属于抽象类
纯虚函数语法:`virtual 返回值类型 函数名 (参数列表)= 0 ;
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main() {
test01();
system("pause");
return 0;
}
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
6.类的底层原理
1.类底层结构
其实类是用结构体实现的,该结构体中保存着成员变量,成员函数的指针,静态成员函数和静态成员函数的指针等。
当我们实例化一个对象时,会复制一份类的数据,我们可以通过这份数据访问所有对象共同指向的成员函数和静态成员。但成员变量每个对象各自一份的,变量的位置是加上了this的偏移,所以每个对象的变量位置不相同。
静态成员函数和成员函数最大的不同是,静态成员函数不能访问非静态的成员变量,而非静态成员函数可以,这是因为非静态成员函数有默认的形参this,通过this找到非静态成员变量。
同样静态成员函数由于没有this指针,也不能给非静态成员函数赋值this,所以也不能调用非静态成员函数。
2.虚函数底层原理
当类中有虚函数时,类在底层的结构体就会有一个虚函数指针,指向虚函数表,表中的每一项都指向对应的虚函数。但子类重写了虚函数,子类将父类的数据继承下来,同时也继承了虚函数指针和虚函数表,子类也会将虚函数表中的重写的那一项指针指向新的虚函数,这样子类就只能访问子类重写的虚函数了。
当子类中有新的虚函数,就会往虚函数表中往下添加表项,并指向对应的虚函数。
结论:
(1) 对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。
(2) 对于单一继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据。
(3) 虚函数表中,先存放父类的虚函数,再存放子类的虚函数
(4) 如果重载了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖。
(5) 对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。
当父类指针指向子类的对象时,能获取数据是因为,子类对象先存放了父类的数据,并将重写的虚函数进行了替换。