指针:
含义:
当变量在赋值符号左边的时候叫:左值(Lvalue)。实际表示的是:存储单元地址
当变量在赋值符号右边的时候叫:右值(Rvalue)。实际表示的是:存储单元里面的数据
地址:系统把内存以一个字节为单位进行划分,分为很多份进行编号。这个编号就是内存地址(存储单元的首地址)
在C中,指针和地址的概念差不多,可以认为指针是一个拥有类型的地址。
一个变量的首地址,称为该变量的:指针。它标志着该变量的存储空间从哪里开始的。
指针变量:
指针变量也是一个变量,只是它是用来存储一个的对象的地址的。
一般情况所说的指针,都是指的是指针变量
指针变量的定义语法:
类型说明符 * 指针变量名{=初始化};
类型说明符:
表示该指针变量用于存储什么类型的存储单元的地址的
*:
指针变量说明符,表示定义的变量是一个指针变量,并非普通变量
指针变量名:
任意合法的C标识符都是可以的
int *p;//int 表示p这个指针变量,所以保存这个地址的存储单元是一个用来存储int类型数据的存储单元,p就是一个指针变量名
//一般情况下,我们称p为int*类型的指针
const char *p ="abcdef";//把字符串的首地址a的地址,赋值给了p
//加const 更加安全,因为"abcdef"为常量字符串
一:指针数组:
指针数组是数组,是用来存放指针的数组
int arr[10]; //整形数组 [int int int int int int int int int int]
char ch[5];//字符数组[char char char char char]
//指针数组
int* arr2[6];//存放整型指针的数组[int* int* int* int* int*nint*]
char* arr[5];/存放字符指针的数组[char * char * char * char * char*] 一级指针字符数组
char** arr1[5];//二级指针字符数组
二:数组指针
数组指针是指针
整型指针-指向整型的指针
int*
字符指针-指向字符的指针
int *p1[10];//数组指针
int (*p2)[10];//指针数组,p2先和*结合,说明p2是一个指针变量,p2可以指向一个数组,该数组有10个元素,每个元素为int类型
int arr[10]={0};
int* p = arr;
int (*p2)[10] = &arr;
整型指针是用来存放整型的地址
字符指针是用来存放字符的地址
数组指针是用来存放数组的地址
char* arr[5]={0};
char* (*pc)[5] = &arr;
char ch = "w";
char* p1 =&ch;
char** p2 = &p1;//二级指针用来放一级指针变量地址的
//不建议的写法:
//建议写法:
数组指针用法:
//一般多维数组中
二维数组的首元素是它的第一行
一级指针传参:
二级指针传参:
三:函数指针 :
指向函数的指针(函数指针是用来存放一个函数的地址)
而(*pf)(2,3);通过函数指针调用Add函数时候*号是不起作用,也可以写成pf(2,3);和写成(***********pf)(2,3);
//其中*号的作用是为了更好理解指针解引用
函数指针使用:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void menu()
{
printf("****************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("******* 0.exit *******\n");
printf("****************************\n");
}
int Add(int x,int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
int ref = pf(x, y);
printf("计算结果为:%d\n", ref);
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
printf("退出计算器!\n");
break;
default:
printf("选择错误!\n");
break;
}
} while (input);
return 0;
}
四:函数指针数组 :
函数指针也是指针
把函数和指针放在数组中,其实就是函数指针数组
//简化代码量,修改起来简单
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x,int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int (*pf)(int, int) = Add;//pf为函数指针
int (* arr[4])(int ,int) = {Add,Sub,Mul,Div};//arr为函数指针的数组
for (int i = 0; i < 4; i++)
{
int ref = arr[i](8, 4);
printf("%d\n", ref);
}
return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void menu()
{
printf("****************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("******* 0.exit *******\n");
printf("****************************\n");
}
int Add(int x,int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
int ref = pf(x, y);
printf("计算结果为:%d\n", ref);
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ref = 0;
//函数指针的数组(转移表)
int (*pfArr[5])(int, int) = {0, Add,Sub,Mul,Div };
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
}
else if(input >= 1 && input <= 4)
{
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ref = pfArr[input](x, y);
printf("%d\n", ref);
}
else
{
printf("选择错误!\n");
}
} while (input);
//int (*pf)(int, int) = Add;//pf为函数指针
//int (* arr[4])(int ,int) = {Add,Sub,Mul,Div};//arr为函数指针的数组
//
//for (int i = 0; i < 4; i++)
//{
// int ref = arr[i](8, 4);
// printf("%d\n", ref);
//}
return 0;
}
五:指向函数指针数组的指针
指向函数指针数组的指针是一个指针
指针指向一个数组,数组的元素都是函数指针
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x,int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ref = 0;
//函数指针数组
int (*pfArr[5])(int, int) = {0, Add,Sub,Mul,Div };
//指向【函数指针数组】的指针
int(*(*ppfArr)[5])(int, int) = &pfArr;//指针数组指向的类型是函数
return 0;
}
六:回调函数:
回调函数就是通过一个函数指针调用的函数,如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数实现方直接调用,而是在特定的事件或者条件发生时候由另外一方调用的,用于对该事件或者条件进行响应。
实现冒泡排序
//比较2个整型元素
//e1指向一个整数
//e2指向另外一个整数
//void* 作为参数可以接受任意类型的数据类型
int cmp_int(const void* e1, const void* e2)//void* 不能直接解引用,也不能加减整数(是无具体类型的指针,可以接受任意类型的地址)
{
return (*(int*)e1 - *(int*)e2); //*(int*)e1 强制转换int类型的指针,再解引用(解引用后即为整数)
}
int main()
{
int arr[] = { 0,9,7,2,6,3,1,4,5,8 };
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
//把数组排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);//回调函数(传入规则函数指针)实现排序
for (int i = 0; i < sz; i++)
{
printf("%d\n",arr[i]);
}
return 0;
}
#include<string.h>
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp( ((struct Stu*)e1)->name, ((struct Stu*)e2)->name );
//也可以解引用:(*(struct Stu*)e1).name - (*(struct Stu*)e2).name
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
//也可以解引用:(*(struct Stu*)e1).age - (*(struct Stu*)e2).age
}
void test01()
{
//测试qsort来排序数据结果
struct Stu s[] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
for (int i = 0; i < sz; i++)
{
printf("%s\n", s[i].name);
}
}
实现自定义冒泡排序
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void *base, int sz,int width,int(*cmp)(const void*e1,const void*e2))
{
//趟数
for (int i = 0; i < sz - 1; i++)
{
int flag = 1;//假设数组是排好序的
//一趟冒泡排序过程
for (int j = 0; j < sz - 1 - i; j++)
{
/*强转为char* 是为了跳的字节最小为1 当j = 1时候为int类型时候刚好跳4字节
,widht是传入的字节(char*)base + j * width*/
//传入的而数据多少字节(j*width)为j*多少字节 传入int 为 j*4个字节(char(*)为单位1个字节)
//cmp的两个参数是待比较的两个元素地址
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
//交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
void test02()
{
int arr[] = { 0,9,7,2,6,3,1,4,5,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
for (int i = 0; i < sz; i++)
{
printf("%d\n", arr[i]);
}
struct Stu s[] = { {"zhangsan",15},{"lisi",30},{"wangwu",25} };
sz = sizeof(s) / sizeof(s[0]);
bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
for (int i = 0; i < sz; i++)
{
printf("%s\n", s[i].name);
}
bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
for (int i = 0; i < sz; i++)
{
printf("%s\n", s[i].name);
}
}
作用域 :
全局变量和局部变量的使用
#include <iostream>
using namespace std;
int a = 10;
void test()
{
int a = 10;
//直接访问的为局部变量
cout<<"局部变量:a="<<a<<endl;
//加::作用域显示为全局变量
cout<<"全局变量:a="::a<<endl;
}
命名空间 :
#include <iostream>
using namespace std;
int a = 10;
namespace A
{
int data = 10;
}
namespace B
{
int data = 20;
}
namespace C
{
int data = 10;
namespace D
{
int data = 20;
}
}
//可以继续在命名空间A下面添加新的成员,并不是重写属于新添加的
namespace A
{
void func()
{
cout<<"你好"<<endl;
}
}
void teste()
{
//错误不能局部定义命名空间
//namespace A
//{
// int data = 10;
//}
//加作用域是最简单安全的方法
cout<<"A::data1" << A::data<<endl;
cout<<"B::data1" << B::data<<endl;
cout<<"C::D::data1" << C::D::data<<endl;
cout<<"A::func" << A::func<<endl;
}
//内部声明函数
namespace E
{
int a = 10;
void set_a(int data);
int get_a(void);
}
//外部显示函数(需要加作用域)
void E::set_a(int data)
{
a = data;
return;
}
void E::get_a(void)
{
return a;
}
void teste1()
{
E::set_a(100);
cout<<"a="<<E::get_a()<<endl;
}
//无命名空间
namespace
{
int a = 10;
void func(){cout<<"111"<<endl;}
void teste()
{
cout<<"a:"<<a<<endl;
}
}
//内部空间取别名
//无命名空间
namespace verylongName
{
int a = 10;
void func(){cout<<"111"<<endl;}
}
void teste2()
{
namespace shortName = verylongName;
cout<<"verylongName::a"<<shortName::a<<endl;
verylongName::func();
shortName::func();
}
namespace F
{
int a = 10;
}
//using是修饰命名空间里面的某些数据成员和方法
void test2()
{
//a来自F命名空间
using D::a;
//防止同名 否则报错
//int a = 20;
cout<<"a="<<a<<endl;
}
//using声明碰到函数重载
namespace AB
{
void func(){}
void func(int x){}
int func(int x,int y){}
}
void test3()
{
using A::func;
func();
func(10);
func(10,20);
}
//using 修饰整个命名空间
namespace CD
{
int a1 = 10;
int b1 = 20;
int c1 = 30;
}
void test4()
{
//声明整个命名空间里面的数据和方法
using namepace CD;//强调的是命名空间名
//先查看当前普通变量a1 如果没有再查看命名空间是否有a1,都没有就会报错!
cout<<"a1"<<a1<<endl;//10
int a1 = 100;
cout<<"a1 = "<< a1 <<endl;//100
}
namespace 命名空间名称
{
//打包的数据和函数
}命名空间只能在全局上定义
命名空间可以嵌套使用(命名空间嵌套命名空间)
命名空间是开发的,随时可以继续添加新的变量到命名空间中
声明和实现可以分离
无名命名空间,意味着命名空间标识符只能在本文件内访问,相当于给这个标识加上static,使其可以作为内部连接(内部连接:当前源文件可以使用,外部连接:其他源文件也能使用)
命名空间别名
using是修饰命名空间里面的某些数据成员和方法
如果命名空间包含一组相同名字的重载的函数,using声明就声明了这个重载函数所有集合using是修饰整个命名空间
结构体:(了解)
c++中的结构体成员可以是函数
c++中定义结构体变量不需要加struct关键字(C语言中需要加)
三目运算符:(了解)
c语言中的三目运算符 a>b ? a:b 返回的是a或者b的值
c++中的三目运算符 a>b ? a:b 返回的是a或者b的引用(a或b的变量名)
const
c语言中const是修饰普通变量是只读变量
1、C++中使用常量初始化const修饰变量 那么改变量就是符号常量
const int num = 10;//num没有空间
num是符号常量 被放入符号常量表中
如果对num取整取地址,这个时候系统才会为num开辟空间,num才会从常量变成变量
const int num = 10;//10为常量
int *p = (int*)#
*p = 1000;
cout<<"*p = "<<*p<<endl;//1000
cout<<"num"<<num<<endl;//10
2.如果以普通变量初始化const修饰的变量 会立即开辟空间(就不会有符号常量表)
int b = 10;const int a = b;
int b = 10;
const int num = b; //b为变量量
int *p = (int*)#
*p = 1000;
cout<<"*p = "<<*p<<endl;//1000
cout<<"num"<<num<<endl;//1000
3.const 修饰全局变量a,如果用常量初始化那么a被分配到文字常量区
c语言全局const会被存储到只读数据段。c++全局const当声明extern或者对变量取地址时,编译器会分配存储地址,变量存储在只读数据段。两个都受到只读数据段的保护,不可修改。
const int a = 10;
void test
{
int *p = (int*)&a;
cout<<"-----001--------"<<endl;
*p = 1000;//段错误
cout<<"-----002--------"<<endl;
cout<<"*p = "<<*p<<endl;//1000
cout<<"num"<<num<<endl;//1000
}
4.const如果修饰的是自定义数据类型(l比如结构体)直接开辟空间
struct Stu
{
int num;
char name[32];
};
void test
{
const Stu lucy ={100,"lucy"};
Stu *p =(Stu*)&lucy;
p->num = 1000;
cout<<p->num<<""<<p->name<<endl;
}
尽量用const替换#define
#define NUM 100
const int NUM = 100;
1.const有类型,可以进行编译器类型的安全检查,#define无类型 ,不能进行类型检查。
2. const有作用域,而#define不重视作用域,默认定义处到文件结尾,如果定义在指定作用域 ,不能作为作用域和结构体、类的成员。#define只能作为全局定义
#define NUM1 10;
namespace A
{
const int a =10;
//#define NUM 10;//错误,不能作为作用域内成员
}
引用&
引用的本质:给已有的变量名取别名
1.引用的定义方式:
int num = 10;
int &b = num;//给num取个别名为b (&b不是取b的地址,只是描述b是num的别名 编译器不会为b开辟新空间)
//操作b等价操作num
void tese()
{
int num =10;
int &b = num;
cout<<"num="<<num<<endl;
cout<<"b="<<b<<endl;
//b和num占用同一空间
cout<<"&num"<&num<<endl;
cout<<"&b"<<&b<<endl;
}
定义步骤:
1、&修饰别名
2、给哪个变量取别名就定义那个变量
3、从上到下整体替换
案例:给数组取别名
int arr[5]={1,2,3,4,5};
int (&new_arr)[5]=arr;
2、引用必须初始化
int &b;//非法的
3、引用一旦确定是谁的别名就不能更改
int num = 10;
int &b = num;
int data = 20;
b = data;//把data值赋给b
4、引用作为函数的参数 可以代替指针变量
引用作为函数的参数的好处:
①:函数内部直接通过引用操作外部变量的值
②:省去了指针的操作
③:函数形参数不拥有新的空间
5、常引用
int &a =10;//错误
const int &a = 10;//10的类型就是常量 cont int
//a就叫常引用 不能通过a修改空间的值
常引用一般作为函数的参数防止函数内部修改外部变量的值
6、引用作为函数返回值类型
①:通过函数返回值在外界操作函数内部申请的空间
②:引用作为函数的返回值类型,可以完成链式操作
引用的本质:常量指针变量
int num = 10;
int &b = num;
//底层实现
//b是只读 *b可读可写
int * const b = &num;
//b = 100;
*b = 100;
8、指针引用(了解)
指针引用的场景
#include<iostream>
using namespace std;
//指针引用的场景
void get_memery1(int **p1)//int **p1 = &p;
{
//*p1 == p
*p1 = (int*)calloc(1, sizeof(int));//在堆区申请空间
**p1 = 100;
}
void get_memery2(int*& p1)//int *&p1 = &p;
{
p1 = (int*)calloc(1, sizeof(int));//在堆区申请空间
*p1 = 100;
}
void test()
{
int *p1 = NULL;
get_memery1(&p1);
cout << "*p1=" << *p1 << endl;
int* p2 = NULL;
get_memery2(p2);
cout << "*p2=" <<*p2<< endl;
}
int main()
{
test();
return 0;
}
内联函数
内联函数性质和特点:
inline 修饰的函数就是内联函数。
inline int my_add(int x,int y)//inline 必须出现在函数的定义处
{
return x+y;
}内联函数为了继承宏函数的效率,没有调用函数时的开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数
内联函数:是在编译阶段完成的替换
特点:
可以保证参数的完整性
有作用域的限制,可以作为类的成员
类中的成员函数(简单的函数) 默认都是内联函数(不用inline)
加了inline的函数(过于复杂)不一定是内联函数,不加inline(简单的函数)也可能是内联函数
内联函数的要求:
不能存在任何形式的循环语句 ;
不能存在过多的条件判断语句;
函数体不能过于庞大;
不能对函数进行取地址操作。
预处理宏的缺陷
预处理器宏存在的问题关键是我们可能以为预处理器的行为和编译器的行为是一样的。当然也是由于宏函数调用和函数调用在外表看起来一样,因为也容易被混淆。其中也会出现问题:有参宏函数是没有类型(#define Add(x,y) x+y),不能保证函数完整性
问题二:
问题三:
预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。
宏函数和内联函数的区别:
宏函数:
预处理阶段完成替换、没有出入栈的开销,不能保证参数的完成性,没有作用域限制,不能作为类成员
内联函数:
编译阶段完成替换、没有出入栈的开销,能保证参数的完成性,有作用域限制,能作为类成员
函数参数
默认参数定义
函数的默认参数从左往右,如果一个参数设置默认参数,那么这个参数后面的都必须要设置默认参数
默认参数一般在函数声明的时候设置
#include <iostream>
using namespace std;
//如果函数调用时候不给b传参数,那么b的值为100
int my_add(int a = 100,int b = 100)
{
return a+b;
}
void test01()
{
int ret = my_add(10,20);
cout << ret << endl;
cout << my_add(10) << endl;
}
int main()
{
test01();
return 0;
}
占位参数 (了解)
C++在声明函数时,可以设置占位参数。占位参数只有在参数类型声明,没有参数声明。一般情况下,在函数内部无法使用占位参数。
函数重载 (重要)
函数重载条件:
同一作用域下的函数名字相同 函数的参数个数、参数类型、参数顺序不同就可以重载。
函数的返回值不能作为重载的条件
函数重载和缺省参数同时出现一定要注意二义性
函数重载的原理:
总结:编译器内部会根据函数参数不一样重新修饰函数名字,所有同意函数名字参数不一样在编译器内部函数名不一样
类
类的定义:是不占空间,只有用类实例化对象时候系统为对象开辟空间
构造函数
构造函数是编译器自动调用。
类实例化对象的时候编译器自动调用构造函数
本质:初始化数据
定义构造函数:
构造函数函数名和类名相同,没有返回值,连void都不能有,但是可以有参数
构造函数的调用时机:
class Data { private: int m_a; public: Data() { m_a = 0; cout<<"无参构造函数调用m_a= "<<m_a<<endl; } Data(int a) { m_a = a; cout<<"有参构造函数调用m_a= "<<m_a<<endl; } }; void test02() { //类实例化对象自动调用构造函数 //1.隐式调用无参构造 Data obj; //2.显示调用无参构造 Data obj1 = Data(); //3.隐式调用有参构造 Data obj3(10); //4.显示调用有参构造 Data obj4= Data(20); //5.隐式转换调用法(只能有一个成员数据) Data obj5 = 30;//Data obj5(30) //6.匿名对象 Data(10);//(当前语句结束匿名对象立即释放) }
有参构造和无参构造的区别:
1.如果用户没有提供无参构造系统会提供一个空的无参构造
2.如果用户提供了无参构造,系统就不会再提供无参构造了
3.如果用户提供有参构造系统就不会再提供无参构造。
注意:任何一个类记得提供无参构造,当写了有参构造没有无参构造,初始化类时候不进行构造函数初始化就会报错,因为系统不会再提供无参构造。
拷贝构造函数:
拷贝构造函数本质:是构造函数
1.知识点引入
久对象给新对象初始化就会调用拷贝构造函数
2.拷贝构造函数定义形式:
Data2(const Data2& obj){}
3.如果系统不提供拷贝构造,系统会提供默认拷贝构造:
默认拷贝构造:单纯的值拷贝(浅拷贝)
4.自定义拷贝构造、默认拷贝构造、默认无参构造的关系
如果定义了无参构造会屏蔽默认无参构造但是不会屏蔽默认拷贝构造
如果自定义了拷贝构造会屏蔽默认构造、默认拷贝构造
5.注意:
定义任何一个类记得实现无参构造、有参构造、拷贝构造
析构函数
对象生命周期结束时候系统自动调用析构函数
析构函数定义形式:
析构函数函数名是在类名前面加“~”组成,没有返回值类型,连void都不能有,不能有参数,不能重载。
总结:
无参构造、有参构造、 拷贝构造、析构函数
1、构造函数分类(有无参数分类)
有参构造、无参构造
2、构造函数分类(类型)
普通构造函数、拷贝构造函数
3.如果一个类中没有指针成员 用户不需要实现拷贝构造、析构函数
4.如果一个类中有指针成员,用户必须实现拷贝构造、析构函数
浅拷贝 和 深拷贝
class Person
{
public:
char* m_name;
public:
Person(const char* name)
{
cout << "调用有参构造" << endl;
m_name = (char*)calloc(sizeof(name)/sizeof(char), sizeof(name));//在堆区申请空间
// m_name = (char*)calloc(1, strlen(name)+1);
if (m_name == NULL)
{
cout << "空间申请失败" << endl;
exit(-1);//退出程序
}
// strcpy(m_name, name);
strcpy_s(m_name, 20, name);
}
Person(const Person& ob)
{
cout << "拷贝构造函数(深拷贝)" << endl;
m_name = (char*)calloc(sizeof(ob.m_name), strlen(ob.m_name));
//m_name = (char*)calloc(1, strlen(ob.m_name) + 1);
if (m_name == NULL)
{
cout << "空间申请失败" << endl;
exit(-1);//退出程序
}
strcpy_s(m_name,20, ob.m_name);
}
~Person()
{
cout << "析构函数" << endl;
//释放指针成员 指向的堆区空间
if (m_name != NULL)
{
cout << "释放内存" << endl;
free(m_name);
m_name = NULL;
}
}
};
int main()
{
Person obj1("张三");
cout << "obj1.name = " << obj1.m_name << endl;
Person obj2 = obj1;//拷贝构造(默认浅拷贝)
cout << "obj2.name = " << obj2.m_name << endl;
return 0;
}
拷贝构造函数的调用时机
拷贝构造函数的调用时机:旧对象给新对象初始化
情形1:
Data3 obj;
Data3 obj2 = obj;//调用拷贝构造情形2:普通对象作为函数的参数
情形3:普通对象作为函数的返回值
VS下会发生拷贝构造
QT和linux不会发生拷贝构造
初始化列表
1.初始化列表的初识
2.类对象作为成员
1.对象成员(A类)构造 先于 自身构造(B类)
2.默认调用对象成员(A类)的无参构造
3.必须使用初始化列表调用对象成员的有参构造
explict关键字
c++提供了关键字:禁止构造函数隐式转换
new和delete
new从堆区申请空间,delete释放堆区空间
1、new为基本类型 申请堆区空间
2.new为基本类型数组申请堆区空间
3.new申请自定义类型数据(结构体、类型对象)
从编译结果构造和析构可以体现先进后出,压栈和出栈规律
4.C++中申请堆区空间,为啥建议使用new、delete而不是malloc,calloc,reallo和free
4.1:给对象申请空间需要做的事情
申请空间(malloc,calloc,realloc)(new)
调用构造函数(new)
4.2:释放对象空间需要做的事情
调用析构函数(delete)
释放堆区空间(delete)(free)
4.3:new能申请空间调用构造函数,而malloc,calloc只能申请空间,delete可以释放空间也能调用析构函数,而free只能释放空间
5、delete不能作用void*修饰的对象指针
直接delete对象无法调用析构函数,需要转换一下void*类型
静态成员
成员数据、成员函数被static修饰就叫静态成员数据、静态成员函数
1.静态成员数据
不管这个类创建多少个对象,静态成员只有一份,这份被所有属于这个类的对象共享
静态成员 是属于类的,而不是具体的某个对象。
静态成员是定义完类的时候就存在了(类外初始化体现了静态成员存在了)。
2.静态成员函数
用途:只能访问私有静态数据(可以类内实现也可以类外实现)
单例模式
单例模式所设计的类只能实例化一个对象 (方便控制实例对象并节约系统资源)
实例化对象(构造函数私有化不能实例化对象)
class Printer { private: int count; //1.不允许Printer实例化对象(把构造函数私有划) //2.定义一个静态对象指针 保存唯一的对象地址 static Printer *p; private: Printer() { count = 0; cout<<"无参构造函数"<<endl; } Printer(const Printer& obj) { count = 0; cout<<"拷贝构造函数"<<endl; } public: //3.定义一个静态成员函数拿到唯一的对象地址方便外界使用 static Printer* getSignleAddr(void) { return p; } //4.具体的功能代码 void printfMsg(char *str) { cout<<"打印:"<<str<<endl; count++; } //获取打印机的数量 int getCount(void) { return count; } }; //定义唯一一个堆区对象(加了作用域属于内类) Printer *Printer::p = new Printer;
成员数据和成员函数的存储方式
1.成员函数不占类的空间
2.静态成员数据不占类的空间
3.普通成员数据会占类的空间
this指针
1、当前对象调用普通成员函数 在成员函数内部产生一个指向该对象的this指针(this指针变量是系统定义的,属于当前对象)
2、this指针目的:说明当前成员函数是属于哪个对象的
3、静态成员函数内部没有this指针(静态成员函数在实例化之前就出现,而且静态成员函数属于类不是属于对象的,所以没有this指针)
4、this指针使用的场景
1.如果成员数据和函数形参同名必须使用this指针
2.this指针完成链式操作
const用法
const修饰成员函数
const修饰对象
const修饰的是常对象,只能调用const修饰的常函数
友元语法
友元函数可以在外部访问操作类的私有数据
friend关键字只出现在声明处 其他类、类成员函数、全局函数都可以声明为友元 友元函数不是类的成员,不带this指针 友元函数可以访问对象任意成员属性,包括私有属性
friend关键字在声明处修饰函数那么该函数就是类的友元。
友元不是类的一部分
1.普通全局函数作为类的友元
2.类的某个成员函数作为另一个类的友元
3.类作为另一个类的友元(这个类能访问另一个所有的私有成员)
4. 友元注意事项
1.友元关系不能继承
2.友元关系是单向的,类A是类B的朋友,但是类B不一定是类B的朋友
3.友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但是类C不一定是类A 的朋友
运算符重载
对已有的运算符进行定义,赋予其另一种功能,以适应不同的数据类型。
用operator关键字及紧跟的运算符组成。
不能修改运算符原本含义
1.重载<<
运算符重载分析步骤:
1.分析运算符左边的运算符对象,如果不是自定义类的对象,只能用全局函数实现运算符重载。
全局函数的第一参数:左边的运算对象,第二个参数:右边对象
2.分析运算符左边的运算对象,如果是自定类对象 ,可以用全局函数、成员函数、实现运算符重载。
成员函数:可以省略一个参数,只需要将运算符右边的对象传递到成员函数中。
2.重载>>
3.成员函数重载==
4.自增自减(++ / --)运算符重载:
全局函数实现:
++obj ----------> operator++(class &ob)
obj++ -----------> operator++(obj,int)
成员函数实现:
++obj -----------> obj.operator++()
obj++ ---------->obj.operator++(int)
后置++
前置++
5.重载=赋值运算符(类中有指针成员 才需要重载=运算符)
如果类中没有指针成员,相同类型的对象 可以直接 = 赋值(浅拷贝 )
如果类中有指针成员 ,相同类型的对象 必须重载 = 赋值(深拷贝)
6.总结:如果类中有指针成员
必须实现拷贝构造(深拷贝)
必须实现析构函数(释放指针成员所指向的空间)
必须重载=运算符(深拷贝)
6.1深拷贝和赋值运算符重载的区别:
7.重载函数调用运算符()
8.不要重载&& || 用户无法实现短路特性
9.不能重载的运算符
继承
1.继承概念
2.继承语法
3.继承方式
4.构造和析构的顺序
5.子类通过初始化列表调用对象
6.子类的成员与父类成员同名
7.子类重定义父类的成员函数
父类构造、析构和operator=不能被继承
8.多继承
9.菱形继承
10.虚继承
多态
1.多态的基本概念
2.虚函数
基类指针指向派生类对象时候,该指针调用的同名函数是基类的
当使用虚函数时候调用的同名函数就是派生类的
3.动态多态
5.虚函数的原理
1.分析Animal类
2.分析派生类dog类
6.纯虚函数
但是可以实例化指针:
7.重载、重写、重定义
8.虚析构函数
以上可以看出子类析构函数未执行
虚析构解决上诉问题
9.虚析构的原理
Animal类:
Dog类:
10.纯虚析构
C++类型转换
1.上下形转换
2.静态转换(static_cast)
1.上行转换(安全支持)
2.下形转换(不安全但支持)
3.基本类型转换(指针、引用不支持)
4.不相关类之间转换(不支持)
3.动态转换(dynamic_cast)
1.上行转换(安全支持)
2.下行转换(不安全不支持)
3.基本类型转换(不支持)
4.常量转换(const_cast)
1.将常 引用转换成非 常引用(去除const属性,支持)
2.将常 指针转换成非 常指针(支持)
3.将常 变量转换成非 常变量(不支持)
5.重新解释转换(reinterpret_cast)