C
以下函数为任意返回值和任意参数
#include <stdlib.h>
a() {
return 15;
}
int main() { int b=a(12, 324, 43); }
指针定义和赋值
#include <stdio.h>
int main(){
int number = 10;
int *flag,*now; //定义两个int类型的指针
int start=0;
int *start_c;
start_c=&start; //&地址运算符将start的地址赋值给start_c
int *number1;
number1=&number;
printf("%d\n",*start_c);//用取值运算符拿到的是内存对应的
printf("%d\n",start);//
printf("%d\n",number);
printf("%d\n",*number1);
printf("%d\n",start_c);//打印出来的是地址,因为start_c是指针类型的变量
printf("%d\n",number1);
for(start_c;start<=number;start_c+=1)
{
}
//因为两个变量在内存中连续,并且是int,4字节
}
输出结果
0
0
10
10
243168552
243168556
指针作为函数参数进行传递,返回值,二级指针
指针作为参数进行传递的时候要注意是局部变量还是全局变量
#include <stdio.h>
#include <stdlib.h>
int exec1(int l,int r);
int exec2(int* l,int* r);
int** exec3(int *l,int *r);
int main()
{
int a=0,i=0,j;
int l=1,r=0;
srand((unsigned int)time(NULL));//timer=NULL时得到当前日历时间
i = 5+(int)(rand()%5);
j=i;
printf("a: %d i: %d",a,i);
printf("l: %d r: %d",l,r);
while(i>0)//
{
i--;
a=1+(int)(rand()%3);
printf("a is %d\n",a);
switch(a)
{
case 1:
exec1(l,r);
printf("exec1经过 : %d 变换,l : %d ,r : %d\n",j-i,l,r);
break;
case 2:
exec2(&l,&r);
printf("exec2经过 : %d 变换,l : %d ,r : %d\n",j-i,l,r);
break;
case 3:
printf("更改前内存值%d\n",&l);
int ** relt;
relt=exec3(&l,&r);
printf("exec3经过 : %d 变换,l : %d ,r : %d\n",j-i,l,r);
printf("更改后内存值%d\n",&l);
printf("更改后relt内存值%d\n",relt);
printf("*取值%d\n",*relt);
printf("**取值%d\n",**relt);
break;
default:
break;
}
}
return 0;
}
int exec1(int l,int r)
{
int tmp;
tmp=l;
l=r;
r=tmp;
return 0;
}
int exec2(int *l,int *r)
{
int * tmp;
tmp=l;
l=r;
r=tmp;
return 0;
}
int** exec3(int *l,int *r)
{
int tmp;
tmp=*l;
printf("tmp is %d\n",tmp);
printf("*l is %d\n",*l);
printf("l is %d\n",l);
printf("&l is %d\n",&l);
*l=*r;
*r=tmp;
int ** rtn;
rtn=&l;
return rtn;
}
a: 0 i: 5l: 1 r: 0a is 2
exec2经过 : 1 变换,l : 1 ,r : 0
a is 3
更改前内存值2137604852
tmp is 1
*l is 1
l is 2137604852
&l is 2137604808
exec3经过 : 2 变换,l : 0 ,r : 1
更改后内存值2137604852
更改后内存值2137604808
*取值2137604852
**取值0
a is 3
更改前内存值2137604852
tmp is 0
*l is 0
l is 2137604852
&l is 2137604808
exec3经过 : 3 变换,l : 1 ,r : 0
更改后内存值2137604852
更改后内存值2137604808
*取值2137604852
**取值1
a is 2
exec2经过 : 4 变换,l : 1 ,r : 0
a is 3
更改前内存值2137604852
tmp is 1 //这是通过int类型tmp通过*赋值后直接取值
*l is 1 //通过*取值
l is 2137604852 //l是指针类型,直接取是l的实际值,也就是一个内存地址
&l is 2137604808 // 取出l的地址,这里取到的是exec3中栈的l的地址值
exec3经过 : 5 变换,l : 0 ,r : 1
更改后内存值2137604852
更改后内存值2137604808// relt的值,和在栈中的地址一致
*取值2137604852//取出一级指针对应的地址
**取值0 //取出二级指针对应的值
指针数组和数组指针
指针数组是数组,指针数组是多个指针形成的数组,
数组指针是指针,指向数组的指针.
int arr[5]={1,2,3,4,5}
实际数据[int,int,int,int,int]
int *parr[5]为指针数组
[int*,int*,int*,int*,int*]
int (*parr2)[5]为数组指针
parr2是指针类型,指向一个5位长度的数组
二维数组
#include "stdio.h"
int main(){
int a[2][3]={{2,4,8},{1,7,9}};
printf("%d\n",a[0][2]);//取出8
printf("%d\n",&a);//打印a的地址
printf("%d\n",a);//a的地址
printf("%d\n",a[1]);//a的第二列开头的地址
printf("%d\n",*a[1]);//第二列开头的取值
printf("%d\n",a[1]+1);//第二列第二个7的地址
printf("%d\n",*(a[1]+1));//取出7
printf("%d\n",*(a+1)+1);//第二列第二个位置
printf("%d\n",*(a+1));//第二列开头
return 0;
}
输出
8
-1129121264
-1129121264
-1129121252
1
-1129121248
7
-1129121248
-1129121252
指向二维数组的指针
int a[2][3]={{2,4,8},{1,7,9}};
int (*parr)[2][3]=NULL;//2行3列,列不能省略,行可以省略
parr=&a;
函数指针和指针函数
函数指针
定义的函数在编译的时候会分配函数一段内存地址,指向这段地址的指针就是函数指针.
举例
int (*p)(int,int);
//因为函数原型是以参数,返回值区分的,所以此处对应
函数返回值 (*变量名) (参数列表);
作用:
1.调用函数 上面函数调用方法为 (*p)(3,5);
2.将函数作为参数传递给其他的函数
void func(int(*p)(int,int),int b,int c);
指针函数
指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针.
类型标识符 *函数名(参数表)
int *f(x,y);
首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有函数返回值,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
表示:
float *fun( );
float *p;
p = fun(a);
指针练习1
/*
写一个函数,接受一个整数数组和数组长度作为参数,并返回数组中的最大值和最小值。
给定函数原型:主要是通过指针作为参数进行训练,注意形参和实参的区别
*/
#include <stdio.h>
#include <stdlib.h>
void findMinMax(int* arr, int length, int* min, int* max);
void findMinMax(int* arr, int length, int* min, int* max) {
for (int i = 0; i < length; i++)
{
if (*min==0)
{
*min = arr[i];
}
if (*min>= arr[i])
{
*min = arr[i];
}
if (max == 0)
{
*max = arr[i];
}
if (*max <= arr[i])
{
*max = arr[i];
}
}
}
int main() {
int a[] = { 32,43,54,123,54,23,65,25 };
int min = 0;
int max = 0;
findMinMax(a,8,&min,&max);
printf("%d\n",min);
printf("%d\n", max);
}
指针练习2
/*
题目:
写一个函数,接受一个字符串作为参数,并计算字符串中的字母、数字和空格的数量,并通过指针参数返回这些统计结果。
要求:
函数的原型为:void countCharacters(const char* str, int* letterCount, int* digitCount, int* spaceCount)
使用指针遍历字符串,并统计字母、数字和空格的数量
将统计结果通过指针参数返回
*/
#include <stdio.h>
#include <stdlib.h>
void countCharacters(const char* str, int* letterCount, int* digitCount, int* spaceCount);
void countCharacters(const char* str, int* letterCount, int* digitCount, int* spaceCount) {
//printf("%d\n", strlen(str));
int len = 0;
while (*(str + len) !='\0')
{
if (*(str + len)==' ')//注意此处必须用单引号,要不然不是char,不进判断
{
*spaceCount += 1;
//printf("空格");
}
if ((*(str + len)>=65&& *(str + len)<=90)|| (*(str + len) >= 97 && *(str + len) <= 122))
{
*letterCount += 1;
}
if ((*(str + len) >= 48 && *(str + len) <= 57))
{
*digitCount += 1;
}
len += 1;
}
}
int main() {
//char a[] = "akjcci aoidfj cVah 34 39489 a09f8Aa9D914 u98yc 98 139r1h19hd9uhca9f 8f198audhf ";
char a[] = "123 abc";
int letterCount = 0;
int digitCount = 0;
int spaceCount = 0;
countCharacters(a, &letterCount, &digitCount, &spaceCount);
printf("字母的数量:%d\n", letterCount);
printf("数字的数量:%d\n", digitCount);
printf("空格的数量:%d\n", spaceCount);
}
字母的数量:3
数字的数量:3
空格的数量:1
指针练习3
/*
题目:编写一个函数,接受一个整数数组的一级指针和数组的长度作为参数,函数的功能是将数组中的奇数元素放在数组的前半部分,偶数元素放在数组的后半部分,并返回数组中奇数元素的个数。
要求:
在函数内部使用一级指针和二级指针来操作数组。
不允许创建新的数组来存储结果,需要在原数组上进行操作。
函数的返回值为奇数元素的个数。
你可以选择使用C语言或C++语言来编写代码。完成后,可以尝试编译和运行代码,验证函数的功能是否正确。
提示:
可以使用两个指针,一个从数组的开头向后遍历,另一个从数组的末尾向前遍历,交换奇数和偶数元素的位置。
使用一个计数器变量来记录奇数元素的个数,并在遍历过程中进行更新。
希望这道题目能帮助你练习和巩固一级指针和二级指针的使用。如果有任何疑问,请随时提问。
*/
//是将数组中的奇数元素放在数组的前半部分,偶数元素放在数组的后半部分,并返回数组中奇数元素的个数。
#include <stdio.h>
int swapListNum(int *arr,int num) {
int changeNum = 0;
int *start = arr;
int *end = arr +num-1;
if (start < end) {
while (start<end)
{
while (*start % 2 == 1 && start < end) {
start++;
}
while (*end % 2 == 0 && start < end) {
end--;
}
if (start < end) {
int temp = *start;
*start = *end;
*end = temp;
changeNum++;
}
}
}
printf("%d\n",changeNum);
}
int main() {
int arr[] = {1,2,3,4,5,6,7,8,9,10,11 };
int length = sizeof(arr)/sizeof(arr[0]);
printf("交换前:");
for (int i = 0; i < length; i++)
{
printf("%d ",*(arr+i));
}
printf("\n");
int sucNum = swapListNum(&arr,length);
printf("交换前:");
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
/*
1 2 3 4 6 5 7 9 8 10 11
交换完成后
1 11 3 9 5 7 6 8 4 10 2
指针练习4
/*
题目:编写一个函数,接受一个整数数组的二级指针和数组的长度作为参数,函数的功能是将数组中的负数元素替换为零,并返回替换的次数。
要求:
在函数内部使用二级指针来操作数组。
不允许创建新的数组来存储结果,需要在原数组上进行操作。
函数的返回值为负数元素替换的次数。
*/
#include <stdlib.h>
#include <stdio.h>
int changNum(int *arr, int len) {
if (sizeof(arr)==0)
{
return 0;
}
int length = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++)
{
int tmp = *(arr + i);
if (tmp >= 1)
{
*(arr + i) = 0;
}
}
}
/*
编写一个函数,接受一个整数数组的二级指针和数组的长度作为参数,函数的功能是将数组中的负数元素替换为-100,并返回替换的次数。
*/
int changNum2(int **arr, int len) {
if (sizeof(arr) == 0)
{
return 0;
}
int *list = *arr;
for (int i = 0; i < len; i++)
{
int tmp = *(list + i);
if (tmp<0)
{
*(list + i) = -100;
}
}
}
int main() {
int a[] = { 2, -5, 3, -8, 1 };
int length = sizeof(a) / sizeof(a[0]);
int sunNum = changNum(&a,length);
int **p2=&a;
int sunNum2 = changNum2(&p2,length);
for (int i = 0; i < length; i++)
{
printf("%d\n",a[i]);
}
}
指针练习5
反转字符串
/*
题目:编写一个函数,接受一个字符串数组的二级指针和数组的长度作为参数,函数的功能是将数组中的每个字符串逆序存储,并返回逆序存储的字符串数组。
要求:
在函数内部使用二级指针来操作数组。
不允许创建新的数组来存储结果,需要在原数组上进行操作。
函数的返回值为逆序存储的字符串数组
*/
#include <stdio.h>
#include <stdlib.h>
char * changeOrder(char ** arr,int len) {
char *tmp = *arr;
for (int i = 0; i < len; i++)
{
int start = i-1;
int end = len-i-1;
if (end > start) {
char tmp_tmp = *(tmp+i);
*(tmp+i) = *(tmp+end);
*(tmp + end) = tmp_tmp;
end--;
start++;
}
}
return *arr;
}
int main() {
char arr[] = "abcde";
char * list = arr;
int lenght = sizeof(arr) / sizeof(char);
//printf("%c\n", arr[1]);
char * rtn=changeOrder(&list, lenght);
for (int i = 0; i < lenght; i++)
{
printf("%c", rtn[i]);
}
printf("\n");
}
内存示意图
指针练习6
/*
有一个字符串变量 str,其值为 "Hello, World!"。你需要编写一个函数,接受一个二级指针作为参数,并将 str 的地址存储在该二级指针所指向的地址中。
void setString(char** ptr, char* str);
要求实现 setString 函数,使得它可以将 str 的地址存储在 ptr 指向的地址中。
*/
#include <stdio.h>
#include <stdlib.h>
void setString(char** ptr, char* str);
void setString(char** ptr, char* str) {
*ptr = str;
}
int main() {
char *a = "helloworld";
char *b = "aaaaa";
char **ptr = &b;
setString(ptr,a);
int len = 0;
while (*((*ptr) + len)!='\0')
{
printf("%c", *((*ptr) + len));
len++;
}
printf("\n");
for (size_t i = 0; i < len; i++)
{
printf("%c",*(*(ptr)+i));
}
}
指针练习7
/*
有一个字符串变量 str,其值为 "Hello, World!"。你需要编写一个函数,接受一个二级指针作为参数,并将 str 的地址存储在该二级指针所指向的地址中。
void setString(char** ptr, char* str);
要求实现 setString 函数,使得它可以将 str 的地址存储在 ptr 指向的地址中。
*/
#include <stdio.h>
#include <stdlib.h>
void setString(char** ptr, char* str);
void setString(char** ptr, char* str) {
*ptr = str;
}
int main() {
char *a = "helloworld";
char *b = "aaaaa";
char **ptr = &b;
setString(ptr,a);
int len = 0;
while (*((*ptr) + len)!='\0')
{
printf("%c", *((*ptr) + len));
len++;
}
printf("\n");
for (size_t i = 0; i < len; i++)
{
printf("%c",*(*(ptr)+i));
}
}
内存图示
综合案例
使用函数指针,二级指针,二维数组的相关知识点,对随机生成的二维数组根据用户的选择改变函数指针的具体处理,最后输出结果,释放内存.
#include <stdlib.h>
#include<time.h>
#include "stdio.h"
int sumRow(int *matrix, int rows, int columns, int rowIndex);
int sumColumn(int *matrix, int rows, int columns, int columnIndex);
//声明函数原型
int main(){
int x,y;
input:
printf("请输入要生成的行数:\n");
scanf("%d",&x);
printf("请输入要生成的列数:\n");
scanf("%d",&y);
if(x <=0||y<=0)
{
printf("数字应该大于0,重新输入");
goto input;
}
//申请内存空间,二级指针
int **xs=(int **)malloc(x*sizeof(int *));
for (int i = 0; i < x; ++i) {
xs[i]=(int *) malloc(y* sizeof(int ));
//申请一级指针的内存
}
srand((unsigned int)time(NULL));
for (int i = 0; i < x; ++i) {
for (int j = 0; j < y; ++j) {
xs[i][j]=rand() % 10 + 1;
}
}
//使用随机数进行填充
printf("\n生成的数组是:\n");
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
printf("%d ", xs[i][j]);
}
printf("\n");
}
int choice;
printf("\n输入选择的操作:\n");
printf("1:求行\n");
printf("2:求列\n");
scanf("%d", &choice);
int index;
printf("操作的索引:");
scanf("%d", &index);
int (*sumFunction)(int *,int,int,int);
//数组指针,行,列,计算的行和列
//定义一个函数指针
switch(choice)
{
case 1:
sumFunction=sumRow;
printf("Sum of row %d: %d\n", index, sumFunction(*xs, x, y, index));
//由于sumFunction的参数需要的是一个指针,所以取出二级指针的值作为参数
break;
case 2:
sumFunction=sumColumn;
printf("Sum of column %d: %d\n", index, sumFunction(*xs, x, y, index));
break;
default:
printf("请检查输入的选项");
}
free(xs);
//释放二维数组
return 0;
}
int sumRow(int *matrix, int rows, int columns, int rowIndex) {
int sum = 0;
for (int j = 0; j < columns; j++) {
sum += *(matrix + rowIndex * columns + j);
//根据内存的相对位置来取出数值进行相加
}
return sum;
}
int sumColumn(int *matrix, int rows, int columns, int columnIndex) {
int sum = 0;
for (int i = 0; i < rows; i++) {
sum += *(matrix + i * columns + columnIndex);
}
return sum;
}
案例二
通过用户输入,把信息放到char数组中,然后用malloc申请内存char类型的指针,将内存指针对应的地址赋值为数组中的值,然后遍历指针,打印对应的值.
#include "stdio.h"
#include "stdlib.h"
#include <string.h>
int main()
{
char buf[1024]; //定义一个数组用来存放输入的字符
char * strArray[1024];//字符串类型指针数组
printf("请输入学生的姓名:");
int i=0;
while(1)
{
scanf("%s",buf);
if(strcmp(buf,"end")==0){
printf("输入完毕");
break;
}
strArray[i]=(char *)malloc(sizeof(buf)+1);//申请buf长度1024的char指针
strcpy(strArray[i],buf);//给char指针对应的地址赋值
i++;
}
for (int j = 0; j < i; ++j) {
printf("%s\n",strArray[j]);
}
}
const关键字
#include <stdlib.h>
#include <stdio.h>
int main() {
const int a=11;
//定义常量
int const b = 9;
//定义常量
const int *c;
//定义常数的指针
int const *d=&b;
const int const *e=&a;
//指向常量的常指针
}
1.常量指针
const修饰的常量指针,该常量不能修改
const 数据类型* 指针变量名
int num=10;
const int* p=#
2.指针常量
指针常量是一个指针类型的常量,指针指向不能修改
数据类型* const 指针变量名;
int a=10;
int b=5;
int* const p=&a;
3.指向常量的常指针
const 数据类型* const 指针变量名;
如果const同时出现在数据类型前,又出现在变量名前
说明指向不能变,而且变量也不能变
const int* const p=10;
```cpp
#include <iostream>
void fun1()
{
#define a 10
const int b = 20;
//#undef a # undef
}
void fun2()
{
//a在编译器就进行了替换
printf("a = %d\n", a);
//printf("b = %d\n", b);//不能获取,因为在局部的栈内
}
int main()
{
fun1();
fun2();
return 0;
}
### 案例三
生成棋盘,通过用户输入输出,确定棋盘n的大小.生成N个指针,每个二级指针生成对应的N个一级指针,初始化为N*N的棋盘大小.
然后利用随机数,生成N内的数字,随机生成位置用原型填充,然后循环打印.
```c
#include "stdlib.h"
#include <stdio.h>
#include <string.h>
#include "time.h"
int **createBoard(int n);
void printBoard(int **pInt, int n);
void initBoard(int **pInt, int n, int qizi);
void freeBoard(int **pInt, int n);
void printBoard(int **pInt, int n) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if(*(pInt[i]+j)==1){
printf("●");
}else if(*(pInt[i]+j)==1){
printf("[]");
}
}
printf("\n");
}
}
int** createBoard(int n) {
int **p=(int **)calloc(n, sizeof(int*));
for (int i = 0; i < n; ++i) {
p[i]=(int *) calloc(n, sizeof(int ));
}
return p;
}
int main(){
srand((unsigned int) time(NULL));
int n=0,qizi=0;
printf("请输入棋盘的大小:\n");
scanf("%d",&n);
int **p=createBoard(n);//初始化n*n个空间,返回二级指针
printf("设置棋子的数量:\n");
scanf("%d",&n);
initBoard(p,n,qizi);//通过随机函数生成棋子,生成的棋子位置赋值为1
printBoard(p,n);
freeBoard(p,n);
return 0;
}
void freeBoard(int **pInt, int n) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
free((pInt[i]+j));
}
}
}
void initBoard(int **pInt, int n, int qizi) {
for (int i = 0; i < qizi; ++i) {
int a=rand()%n+1;
int b=rand()%n+1;
printf("%d\n",*(pInt[a]+b));
}
}
案例四
实现去除字符数组中的空格
#include <stdio.h>
#include <stdlib.h>
/*
获取字符数组的长度
*/
int stringlen(char * p1)
{
int len = 0;
char * tmp = p1;
while (0<1)
{
if (*tmp=='\0')
{
return len;
}
tmp += 1;
len += 1;
}
}
/*
需要去除空格的数组,数组的长度,先申请指定相同长度的空间,然后循环长度次数,如果不为空就复制给新申请的空间
并且记录数+1,循环完毕将记录数循环添加到result空间中,最后添加一个结束符,返回结果
首先
*/
char * stringTrim(char * p1,int len)
{
int leng = len;
char *tmp = (char *)malloc(len * sizeof(char));
int tmplen = 0;
for (int i = 0; i < len; i++)
{
char * fortmp = *(p1 + i);
if (fortmp != ' ')
{
*(tmp+tmplen) = *(p1 + i);
//printf("%c\n", *tmp + tmplen);
tmplen += 1;
}
//printf("%c\n", *(p1 + i));
}
char *result = malloc(tmplen * sizeof(char)+1);
for (int j = 0; j < tmplen; j++) {
char * result_tmp = *(tmp + j);
*(result + j) = result_tmp;
}
*(result + (tmplen)) = '\0';//最后添加结束符
free(tmp);
return result;
}
int main()
{
char p1[]= " alkjdf ";
char *p = "abcde";
int len;
len = stringlen(p1);
char* uselen=stringTrim(p1, len);
printf("%s\n", uselen);
return 0;
}
字节对齐
主要为了提高计算机的效率
struct Student //定义结构体
{
int num;
char sex;
short age;
} stu1;
内存中的分布
| num | num | num | num |
| sex | | | |
| short | short | | |
对齐规则:
- 结构体变量的首地址是最长成员长度的整数倍
- 结构体的总长度是最长成员长度的整数倍
- 如果成员变量长度超过了处理器的位数,就以处理器的位数为对齐单位
- 每个成员变量的首地址一定是最长成员长度的整数倍
#include <stdio.h>
// 定义一个结构体
struct MyStruct {
char c;
int i;
double d;
};
int main() {
struct MyStruct myStruct;
printf("Size of MyStruct: %zu\n", sizeof(struct MyStruct));
printf("Address of c: %p\n", &myStruct.c);
printf("Address of i: %p\n", &myStruct.i);
printf("Address of d: %p\n", &myStruct.d);
return 0;
}
/*
Size of MyStruct: 24
Address of c: 0x7ffd18be6a50
Address of i: 0x7ffd18be6a54
Address of d: 0x7ffd18be6a58
*/
结构体 MyStruct 的大小为 24 字节,其中字符型变量 c 占用 1 字节,整型变量 i 占用 4 字节(在大多数系统上是这样的),双精度浮点型变量 d 占用 8 字节。此外,可以观察到各个成员变量的地址按照字节对齐的规则进行了排列.
内联函数
内联函数,就是把函数调用地方的代码直接替换成函数代码,减少了函数调用过程中的出栈入栈操作.
内联函数的定义通常放在头文件中,以便在需要使用该函数的地方进行内联展开。编译器在遇到内联函数的调用时,会将函数调用处的代码替换为函数体的代码。
内联函数的展开可能会增加代码的长度,但可以减少函数调用的开销,特别是在函数体较小且频繁调用的情况下。
内联函数的优点包括减少函数调用的开销、增加程序的执行速度,特别是对于短小的函数。内联函数的展开可能会导致代码膨胀,特别是当内联函数较大或频繁调用时,这可能会增加可执行文件的大小。过度使用内联函数可能会导致代码可读性的降低,因为函数体分散在不同的调用点,使得代码难以理解和维护。对于递归函数、包含循环或复杂控制流的函数,通常不适合内联展开。
树的定义 ,前中后序遍历
#include <stdio.h>
#include <stdlib.h>
typedef int TreeNodeData;
typedef struct TreeNode {
TreeNodeData data;
struct TreeNode * leftTreeNode;
struct TreeNode * rightTreeNode;
}TreeNode;
/*
创建新节点
*/
TreeNode* createNode(int value) {
TreeNode* node=(TreeNode*)malloc(sizeof(TreeNode));
node->data = value;
node->leftTreeNode = NULL;
node->rightTreeNode = NULL;
return node;
}
// 插入结点
TreeNode* insertNode(TreeNode* rootNode, int value) {
if (rootNode==NULL)
{
return createNode(value);
}
if (value > rootNode->data) { rootNode->rightTreeNode = insertNode(rootNode->rightTreeNode,value); }
else if (value < rootNode->data) { rootNode->leftTreeNode = insertNode(rootNode->leftTreeNode,value); }
return rootNode;
}
// 前序遍历
void preorderTraversal(TreeNode* rootNode) {
if (rootNode==NULL)
{
return;
}
printf("%d\n",rootNode->data);
preorderTraversal(rootNode->leftTreeNode);
preorderTraversal(rootNode->rightTreeNode);
}
// 中序遍历
void inorderTraversal(TreeNode* rootNode) {
if (rootNode == NULL)
{
return;
}
inorderTraversal(rootNode->leftTreeNode);
printf("%d\n", rootNode->data);
inorderTraversal(rootNode->rightTreeNode);
}
// 后序遍历
void postorderTraversal(TreeNode* rootNode) {
inorderTraversal(rootNode->leftTreeNode);
printf("%d\n", rootNode->data);
inorderTraversal(rootNode->rightTreeNode);
}
int main() {
TreeNode* rootNode;
rootNode = createNode(5);
insertNode(rootNode,8);
insertNode(rootNode,12);
insertNode(rootNode, 13);
insertNode(rootNode, 7);
insertNode(rootNode, 6);
insertNode(rootNode, 9);
printf("前序排序\n");
preorderTraversal(rootNode);
printf("中序排序\n");
inorderTraversal(rootNode);
printf("后序排序\n");
postorderTraversal(rootNode);
}
CPP
bool类型
#include <stdio.h>
//bool 是cpp中的新增类型
//true或false,非零为真
int main()
{
bool a = true;
bool b = false;
bool c = 123;
printf("%d\n",sizeof(bool));
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
return 0;
}
/*
输出为
1
1
0
1
*/
内联函数
/*
内联函数:
内联函数是一个函数,通过内存膨胀的方式,空间换时间,提高程序运行的速度
把小批量频繁调用的函数直接插入到代码调用的地方,函数没有了出栈入栈的调用耗费.
语法:
inline 返回值类型 函数名(参数列表)
只要在返回值前加上inline,
要求,函数不能太长,简单的赋值或者返回值,频繁调用的推荐写成内联函数
*/
#include <stdio.h>
inline void func(int num);
int main()
{
func(10);
return 0;
}
inline void func(int num)
{
printf("%d\n", num);
}
函数重载
/*
* 函数重载
*
在同一个项目中第一多个函数名相同,但是参数列表不同,返回值类型不关注
*/
#include <stdio.h>
int func1(int n) { return 10 * n; }
float func1(int a, int b) { return a * b * 1.1; }
float func1(float a) { return a; }
int main()
{
int a = func1(10);
printf("%d\n", a);
//func1(3.14);编译报错,3.14默认是double类型,二义性
}
函数参数缺省
/*
*
* 给参数设置默认值
*/
#include <iostream>
namespace SPACE_1 {
int funct() { return 10; }
int funct(int a, int b = 12) { return b * a; }
}
int main() {
using namespace std;
int c=SPACE_1::funct(12);
cout << c << endl;
return 0;
}
引用
/*
* 引用:对一个变量或者对象起别名
* 定义引用的时候必须初始化,把别名绑定到了原来的变量上
*/
/*
* 指针*和引用&的区别
* 两个都是指向一个变量的内存地址
* 指针通过取值符号*,通过指针存储的地址值取出地址值对应的数值
* 引用的形式和变量一样,如何通过变量取值取地址就怎么通过引用取.
*
*/
#include <stdio.h>
int main()
{
int a = 10;
int& num = a;
int* p = &a;
printf("%d\n", &a);
printf("%d\n", p);
printf("%d\n", &num);
return 0;
}
引用作为参数
struct Teacher
{
char name[64];
int age;
};
void printfT(Teacher *p) {
p->age = 99;
}
void printfT(Teacher &p) {
p.age = 98;
}
void printfT3(Teacher p) {//这里的p和t是两个不同的值,在栈内修改p的值并不会同步到t
p.age = 97;
}
/*
1)引用作为其它变量的别名而存在,因此在一些场合可以代替指针
2)引用相对于指针来说具有更好的可读性和实用性
*/
void swap1(int &a,int &b) {
int tmp = a;
a=b;
b = tmp;
}
void swap2(int *a,int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
struct Stud
{
int & age;
};
#include <iostream>
using namespace std;
int main() {
Teacher t;
t.age = 10;
printfT(&t);
printf("%d\n",t.age);
printfT(t);
printf("%d\n", t.age);
printfT3(t);
printf("%d\n", t.age);
int g = 10;
int f = 1;
printf("%d:%d\n", g, f);
swap1(g,f);
printf("%d:%d\n", g, f);
swap2(&g,&f);
printf("%d:%d\n", g, f);
int & h = g;
int & i = g;//多个引用指向同一个变量
printf("%d\n", h);
h = f;
printf("%d\n", &h);
//引用在C++中的内部实现是一个常指针,引用所占用的空间大小与指针相同。
printf("%d\n",sizeof(Stud));
}
引用作为返回值
命名空间
#include <iostream>
namespace NameSpaceA {
int a = 0;
}
namespace NameSpaceB {
int a = 1;
namespace NameSpaceC {
int a = 11;
}
}
int main() {
using namespace std;
using namespace NameSpaceA;
cout << a << endl;
using namespace NameSpaceB;
cout << NameSpaceB::a << endl;
cout << NameSpaceB::NameSpaceC::a << endl;
}
/*
* 引用:对一个变量或者对象起别名
* 定义引用的时候必须初始化,把别名绑定到了原来的变量上
*/
/*
* 指针*和引用&的区别
* 两个都是指向一个变量的内存地址
* 指针通过取值符号*,通过指针存储的地址值取出地址值对应的数值
* 引用的形式和变量一样,如何通过变量取值取地址就怎么通过引用取.
*
*/
#include <stdio.h>
int main()
{
int a = 10;
int& num = a;
int* p = &a;
printf("%d\n", &a);
printf("%d\n", p);
printf("%d\n", &num);
return 0;
}
cin和cout
/*
* cin和cout是对象方法
*/
#include <iostream>
//using namespace std;
int main() {
//using namespace std; //1.使用命名空间
using std::cin;//2.使用命名空间下的个别方法
using std::cout;
using std::endl;
std::cout << "a" << std::endl;//3.通过命名空间调用
int num;
cin >> num;
cout <<num << endl;
}
创建对象和回收对象
#include <iostream>
int main()
{
//1.申请单个内存
int* p1 = new int;
*p1 = 10;
//2.申请单个内存并且初始化
int* p2 = new int{10};
std::cout <<"*p2=" <<*p2 << std::endl;
//3.批量申请
int* p3 = new int[10];
for (size_t i = 0; i < 10; i++)
{
p3[i] = i;
}
delete p1;
delete p2;
delete[] p3;//批量释放内存,必须是首地址,否则释放不完全,从p3释放到[10]这个标志位
//new
return 0;
}
类和对象
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
using namespace std;
//定义类
/*
* 类和结构体的区别?
* 类和结构体是不一样的,结构体c和cpp都有,类允许出现函数,结构体没有函数
* 在Cpp中,可以用class和struct定义一个类,c语言中不允许
* Cpp中也可以用struct声明结构体,
* 在Cpp中用class定义类,用struct
class定义的成员默认是私有的权限,用struct默认是公开的权限
* 使用class继承默认也是私有的,struct默认继承也是公开的
*
*/
class 类名
{
//默认是私有的权限
//成员:1.数据 2.行为(函数)
//访问权限修饰关键字
public: //公开的权限,类的内部和外部都能访问
int a = 0;
private://私有的,只能在类的内部使用
int b = 10;
protected://子类可以使用,类内部可以使用,父类不能使用.
int c = 11;
};
class Sheep {
public:
char name[10];
char name1[10] = "0123232";
char name2[10] = {'a','b'};
int hi;
private: int age;
public:
void speak();//定义函数原型
void eat() {//正常定义函数
cout << "吃" << endl;
}
protected:
void SpeakEnglish() {
cout << "English" << endl;
}
};
void Sheep::speak() {//通过类名::函数名来指定函数作用于,类似名称空间
cout << "speak" << endl;
}
int main() {
Sheep xiYangYang;
strcpy(xiYangYang.name,"喜羊羊");//赋值给name
char aa[10] = "啦";
xiYangYang.hi =12;
//对象指针
Sheep* p1;
p1 = &xiYangYang;//指针赋值
p1->eat();//指针通过箭头访问
}
String类
#include <string>
/*
* string是Cpp的类,string.h是c的,兼容c
*/
using namespace std;
int main() {
string sr;
sr = "adfa";
sr.length();
sr.clear();
}
构造函数
不加访问修饰符的变量和函数默认都是private
#include <iostream>
class MyClass
{
public:
int age;
int hei;
private:
int val;
public:
MyClass() {}//系统回默认生成,并且这个构造函数是内联函数
MyClass(int n,int y) {//正常定义,如果自己定义了函数,默认的构造函数久不会生成
val = n;
age = n;
}
MyClass(int a);//声明函数原型
MyClass(int i, int j, int h);
};
MyClass::MyClass(int n) {//
//外部定义构造函数
age = n;
val = 10;
}
MyClass::MyClass(int i, int j, int h) :age(i), val(h), hei(j) {};//成员初始化列表,把必须初始化的变量进行初始化
int main() {
MyClass obj;
using namespace std;
obj.age = 10;
cout << obj.age << endl;
MyClass obj_0;
MyClass obj_1(12,23);
}
;
析构函数
#include <iostream>
/*
* 主要用于对象的生命周期结束时进行清理,系统可以自动调用析构函数
*
* 1.函数名和类名相同,在前面加上~
* 2.没有返回值类型和返回值,也没有参数
* 3.有低保,系统回给默认的.
* 4.创建对象的时候系统调用了构造函数,
* 5.生命周期结束后会自己调用析构函数
* 6.析构函数必须是公有属性下,因为需要系统调用
* 7.一个类只能有一个析构函数
* 8.先进行构造的对象后进行析构
*/
using namespace std;
class MyClass {
public:
int age;
~MyClass() { cout << "" << endl; }
};
int main() {
MyClass obj_0;
obj_0.~MyClass();
}
拷贝构造函数
/*
* 拷贝构造函数
* 通过拷贝构造函数完成一个复制的过程
* 第一个参数必须是本类对象的引用
* 先是构造函数,然后才能是拷贝构造函数
* 有低保,可以自定义,直接调用低保就是走的浅拷贝
* 应用场景:
* 1.使用一个对象对另一个对象进行初始化
* 2.使用一个对象构造另外一个对象的时候
* 3.函数的参数是类的对象
* 4.函数的返回值是类的对象
*/
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {};
MyClass(int a,int b) {};
~MyClass() { cout << "" << endl; }//析构函数
//MyClass(MyClass& obj) { cout << "拷贝构造函数" << endl; }//拷贝构造函数
MyClass(int a,MyClass& obj) {}//不是拷贝构造函数,因为不是第一个
};
void test1(MyClass a) {
};
/*
* 调用test2,触发的函数顺序
* 1.无参构造,2.拷贝构造函数,3.析构函数,4.析构函数
* 过程:
* 1.MyClass obj;调用无参构造
* 2.return的时候,obj函数调用完成之后就会跟着栈内存消失
* 3.将返回的对象用拷贝构造函数返回给test121这个变量,这里又调用了一次
* 4.第一个析构函数在栈内存消失的时候就会调用
* 5.第二个是在main函数结束的时候调用析构
*/
MyClass test2() {
MyClass obj;
return obj;
};
/*
* 这里直接返回的话没有调用拷贝构造函数,匿名对象返回给test332
*/
MyClass test3() {
return MyClass();
};
int main() {
MyClass m0;
//MyClass m2(m0);这里会调用copy构造函数,因为调用
test1(m0);//这里也会调用拷贝构造函数,因为函数的参数是类的对象
MyClass test121 = test2();
MyClass test332 = test3();
}
/*
* 深拷贝和浅拷贝
* 默认都是浅拷贝
* 浅拷贝遇见指针之类的会无脑抄袭,引发问题
* 深拷贝需要根据情况自己实现,自己重新申请内存进行复制
* 如何防止默认浅拷贝发生?
* 声明一个私有的拷贝构造函数,不定义,主动让程序报错
* 这样调用拷贝构造的时候就会报编译异常,然后再去实现深拷贝
*/
this指针
/*
this 是一个特殊的指针,它指向当前对象的地址。它是一个隐式的指针,在成员函数内部自动可用,用于访问当前对象的成员。
功能:1.解决命名冲突:当类的成员变量和成员函数的参数名与类的成员变量同名时,可以使用 this 指针来解决命名冲突,以明确指示要操作的是成员变量还是参数
2.在成员函数内部访问对象的成员:在成员函数内部,通过 this 指针可以直接访问当前对象的成员变量和成员函数。这是因为成员函数被调用时,会自动传入当前对象的地址作为 this 指针。
*/
class MyClass {
public:
void setX(int a) {
this->s = a;
}
int getX() {
return this->s;
}
private:
int s;
};
/*
只有在非静态成员函数中才能使用 this 指针。静态成员函数是与类关联的,不属于特定的对象,因此在静态成员函数中无法使用 this 指针。
this 指针的使用是隐式的,编译器会自动插入它。因此,在大多数情况下,不需要显式地使用 this 指针,只需直接使用成员变量和成员函数即可。
*/
#include <iostream>
int main()
{
using namespace std;
MyClass my;
my.setX(10);
cout << my.getX() << endl;
return 1;
}
类型别名
typedef 原类型 新类型名;
typedef int myInt; // 为 int 类型起别名 myInt
typedef double myDouble; // 为 double 类型起别名 myDouble
myInt num = 10;
myDouble value = 3.14;
函数模板
/*
模板的主要思想是通过参数化类型,使得代码可以在编译时根据具体的类型生成对应的函数或类。
通过使用模板,可以编写一次代码,然后在不同的类型上进行实例化,从而避免了重复编写相似的代码。
*/
/*
模板分为两种:
1.函数模板:定义一个通用的函数,其中的参数或返回值可以是任意类型。在函数模板中,类型参数用于表示可以是任意类型的占位符。
通过在调用函数时指定具体的类型,编译器会自动生成对应类型的函数。
*/
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
template <typename T>
class Stack {
private:
T* data;
int size;
public:
Stack() { /* 构造函数的实现 */ }
void push(T element) { /* 入栈操作的实现 */ }
T pop() { /* 出栈操作的实现 */ }
};
#include <iostream>
int main() {
using namespace std;
int rst= max(12, 3);
double rst2 = max(12.3, 45.1);
cout << rst << endl;
cout << rst2 << endl;
Stack<int> intStack; // 实例化为存储int类型的Stack类
Stack<double> doubleStack; // 实例化为存储double类型的Stack类
//可以实现对不同类型的数据进行通用的操作,提高了代码的重用性和可扩展性。
return 0;
}
类模板
template <typename T>
class Stack {
private:
T* data;
int size;
public:
Stack() { /* 构造函数的实现 */ }
void push(T element) { /* 入栈操作的实现 */ }
T pop() { /* 出栈操作的实现 */ }
};
int main() {
Stack<int> intStack; // 实例化为存储int类型的Stack类
Stack<double> doubleStack; // 实例化为存储double类型的Stack类
return 0;
}
常函数和抽象数据类型
常函数:函数的参数列表后加上const,用来声明一个函数为常量成员函数简称常函数,因为常函数是const修改的
const 修饰符可以用于类的成员函数,用于指定该函数不会修改类的成员变量。
在成员函数声明和定义中添加 const 修饰符,可以使其成为类的常量成员函数。(声明和定义都需要添加const关键字)
const 修饰符的作用是向编译器发出承诺,保证该函数在执行过程中不会修改对象的状态,也称为只读成员函数。
常量成员函数具有以下特点:
对成员变量是只读的:常量成员函数承诺不会修改类的成员变量。在函数体内,对于非静态成员变量的修改操作会被编译器报错。static修饰的可以进行修改的原因是:static是所有变量共有的.因为常量成员函数被认为对对象是只读的,不会引起对象状态的变化。
常量成员函数只能调用常量成员函数或者静态函数:常量成员函数只能调用其他常量成员函数。这是因为在常量成员函数中,对象被视为只读的,因此不允许调用可能会修改对象状态的非常量成员函数。
优点:
1.明确约束:通过添加 const 修饰符,明确表示该函数不会修改对象的状态,提供了对用户的承诺。
2.更安全:常量成员函数在编译期间会接受额外的静态检查,如果在函数体内尝试修改成员变量,将会导致编译错误,从而提供了更高的安全性。
3.扩展性:常量成员函数可以在常量对象上调用,这对于某些场景非常有用。
定义一个stack.h声明用到的类,
#ifndef STACK_H_
#define STACK_H_
typedef unsigned long Item;//给无符号long起一个别名
class Stack {
private:
enum { MAX = 10 };//定义一个枚举,MAX=10
Item items[ MAX ];//初始化一个item为10
int top;
public:
Stack();//构造函数
bool isEmpty() const;//定义一个常量成员函数
/*
常量成员函数说明:
1.不会修改成员变量的值 2.可以访问成员变量 3.只能调用常量成员函数
不会引起该对象的状态变化
优点:可读性,安全性,约束性
*/
bool isFull() const; //常函数
bool push(const Item &item);//入栈
bool pop( Item &item);//出栈
};
#endif
声明实现类stack.cpp
#include "stack.h"
Stack::Stack() { top = 0; }
bool Stack::isEmpty() const { return top == 0; }
bool Stack::isFull() const { return top == 10; }
bool Stack::push(const Item & item) {
if (top < MAX) { items[top++] = item; return true; }//赋值给当前栈顶指向的元素,然后上调栈顶
else
{
return false;
}
}
bool Stack::pop(Item &item) {
if (top > 0) { item=items[--top]; return true; }//先向下移动栈顶标签,然后赋值
else
{
return false;
}
}
定义一个main.cpp
#include <iostream>
#include "stack.h"
int main() {
using namespace std;
Stack sta;
Item p1;
while (true)
{
cout << "请输入进行的操作:a为push,p为pop,q为退出" << endl;
char useinto;
cin >> useinto;
switch (useinto)
{
case 'a':
cout << "请输入入栈的long值" << endl;
if (!sta.isFull()) {
cin >> p1;
if (p1)
{
sta.push(p1);
cout << p1<<"push" << endl;
}
}
else {
cout << "栈满了" << endl;
}
break;
case 'p':
if (!sta.isEmpty())
{
sta.pop(p1);
cout << p1 << "pop" << endl;
}
else {
cout << "栈空了" << endl;
}
break;
case 'q':
break;
default:
break;
}
}
}
程序的内存区域的划分和作用
内存区域通常被划分为以下几个部分:
1.代码区(Text Segment):
代码区存储了程序的机器码指令,也称为可执行代码。代码区通常是只读的,用于存放程序的指令集。代码区在程序加载到内存时被分配,并在整个程序的执行期间保持不变。
2.数据区(Data Segment):
数据区存储了全局变量、静态变量和静态常量。
数据区分为以下几种类型:
全局初始化数据区:存储全局变量和静态变量的初始化值。
全局未初始化数据区(BSS):存储全局变量和静态变量的未初始化值,由系统自动初始化为0。
静态常量区:存储静态常量的值,通常是不可修改的。
数据区在程序加载时被分配,直到程序结束时才被释放。
3.堆(Heap):
堆用于动态分配内存,用于存储程序运行时动态创建的对象和数据。堆的大小可以动态增长或缩小。程序员通过动态内存分配操作符(如 new、delete 或 malloc、free)来管理堆内存。堆的管理由程序员负责,需要手动分配所需的内存空间,并在不再需要时手动释放内存。
4.栈(Stack):
栈用于管理函数的调用和局部变量的内存分配。栈是一种后进先出(LIFO)的数据结构,存储函数的参数、局部变量和返回地址等信息。
函数调用时,相关数据被压入栈中,函数执行完毕后被弹出。栈的大小是有限的,由操作系统或编译器预先分配。栈的管理是由编译器自动处理的,程序员无需手动操作。
5.常量区:
常量区存储程序中的字符串常量和其他常量数据。常量区通常是只读的,存储在静态存储区域。字符串常量和其他常量在程序编译时就被确定,并在整个程序的执行期间保持不变。
堆栈管理
堆栈(stack)是一种用于管理函数调用和局部变量的内存区域。
堆栈的结构:
堆栈是一块连续的内存区域,以固定的大小分配给每个线程。
堆栈的大小在程序编译时就确定了,通常较小,但足够管理函数调用和局部变量。
堆栈采用先进后出(LIFO)的原则,最后进入堆栈的数据最先被访问和处理。
1.函数调用的堆栈管理:
当一个函数被调用时,它的参数、局部变量和返回地址等信息都会被压入堆栈。压栈的过程包括将参数值复制到堆栈中的相应位置,并将当前函数的返回地址压入堆栈作为返回时的跳转地址。被调用函数执行完毕后,它的相关数据会从堆栈中弹出,控制流返回到调用点继续执行。
2.局部变量的堆栈管理:
每个函数在堆栈上分配一定的空间用于存储局部变量。当一个函数被调用时,局部变量的空间会被分配,当函数执行完毕后,这些空间会被释放。局部变量的生命周期与函数的执行过程相对应,当函数退出时,局部变量的内存会自动释放。
3.堆栈溢出:
堆栈的大小是有限的,如果函数调用过深或局部变量过多,可能导致堆栈溢出。堆栈溢出会导致程序崩溃,因为超出堆栈大小的部分可能会覆盖其他内存区域,引发未定义行为。需要注意的是,堆栈管理是由编译器和操作系统自动处理的,程序员无需显式操作堆栈。编译器负责在函数调用时分配堆栈空间,管理函数的调用和返回过程。操作系统负责分配和管理线程的堆栈空间。
案例
#include<iostream>
using namespace std;//c++的命名空间
class circle
{
public:
double r;
double pi = 3.14;
double area = pi * r*r;
};
int main()
{
circle pi;
cout << pi.area << endl; //乱码
cout << "请输入area" << endl;
cin >> pi.r;
cout << pi.area << endl; //乱码
//因为pi是在函数调用的时候放到栈中的,这时候r可能为任何值
system("pause");
return 0;
}
多态
#include <iostream>
using namespace std;
class Fat {
public:
int age;
int val = 10;
Fat();
~Fat();
void func();
};
void Fat::func() {
cout << "Fat:func" << endl;
}
class Son :public Fat {
public:
Son();
~Son();
Son(int a);
int age;
int vall = 1;
void func();
};
void Son::func() { cout << "Son:func" << endl; }
Fat::Fat() { cout << "Fat()" << endl; }
Fat::~Fat() { cout << "~Fat()" << endl; }
Son::Son() { cout << "Son()" << endl; }
Son::~Son() { cout << "~Son()" << endl; }
class Son2 :public Fat {
public:
int age;
int vall = 1;
void func();
};
void Son2::func() {
cout << "Fat:func" << endl;
}
int main() {
Son obj;
obj.func();//同名的函数调用派生类自己的,函数重载,优先选择子类实现,如果子类没有实现就调用父类实现
}
抽象函数
抽象函数是指在基类中声明但没有提供实现的函数,下面的draw属于抽象函数的范围,只有声明没有定义,子类中有自己的额定义
#include <iostream>
class Shape {
public:
void draw();//抽象函数
void display() const {
std::cout << "调用display" << std::endl;
}
};
class Rectangle :public Shape {
public:
void draw()const {
std::cout << "调用Rectangle draw" << std::endl;
}
};
class Circle : public Shape {
public:
void draw()const {
std::cout << "调用Circle draw" << std::endl;
}
};
int main() {
Circle *cir = new Circle();
cir->draw();
cir->display();
delete cir;
return 0;
}
虚函数
虚函数是抽象函数的范围,有默认的实现,派生类可以选择是否重新定义该函数,以提供特定的实现为动态绑定,它使得在运行时根据对象的实际类型来确定调用的函数。。如果没有实现就是纯虚函数.
class BaseClass {
public:
virtual void virtualFunction() {
// 虚函数的默认实现
}
};
class DerivedClass : public BaseClass {
public:
void virtualFunction() override {//注意此处的override
// 派生类的特定实现
}
};
通过输入选项实现多态,绑定子类的方法
#include <iostream>
using namespace std;
/*
* 系统会根据类的虚函数维护虚表
* 所有的虚函数都会存在其中,子类中有同名函数的话,子类会重写
* 不是virtual的话父类函数实现会隐藏
* 虚函数表不会被子类继承,只会继承虚函数,子类根据自己情况生成虚函数
*/
class Fat {
public:
Fat();
~Fat();
int age;
int val = 10;
virtual void func();//加上之后就是动态绑定
};
void Fat::func() {
cout << "Fat:func" << endl;
}
class Son :public Fat {
public:
Son();
~Son();
Son(int a);
int age;
int vall = 1;
void func();
};
void Son::func() { cout << "Son:func" << endl; }
Fat::Fat() { cout << "Fat()" << endl; }
Fat::~Fat() { cout << "~Fat()" << endl; }
Son::Son() { cout << "Son()" << endl; }
Son::~Son() { cout << "~Son()" << endl; }
class Son2 :public Fat {
public:
int age;
int vall = 1;
void func();
};
void Son2::func() {
cout << "Son2:func" << endl;
}
int main() {
Fat* fat;
Son obj;
obj.func();//同名的函数调用派生类自己的,函数重载,优先选择子类实现,如果子类没有实现就调用父类实现
Son2 obj2;
char a;
cout << "输入需要绑定的子类;a或者b" << endl;
cin >> a;
cout << a << endl;
if (a == 'a') {
fat = &obj;
fat->func();
}
if (a == 'b') {
fat = &obj2;
fat->func();
}
}
/*
Fat()
Son()
Son:func
Fat()
输入需要绑定的子类;a或者b
b
b
Son2:func
~Fat()
~Son()
~Fat()
/*
虚析构函数
当用父类指针接收子类对象时候,使用delete回收变量的时候调用的是父类的析构函数,这时候会产生内存碎片,所以需要将父类的析构函数也定义为虚函数.
virtual ~Fat();//在父类的虚构函数前加上virtaul关键字,这样就会绑定子类的析构函数,然后按照调用顺序
纯虚函数
纯虚函数是一个虚函数,虚函数不一定都是纯虚函数,
纯虚函数和虚函数的区别:
1.虚函数(Virtual Function):
虚函数是在基类中声明的,它可以在派生类中被重新定义。
虚函数通过在基类中使用 virtual 关键字来声明。
虚函数的目的是实现运行时多态性,允许在基类指针或引用上调用派生类的特定函数。
虚函数可以有默认的实现,即基类中可以为虚函数提供默认的实现代码。
2.纯虚函数(Pure Virtual Function):
纯虚函数是在基类中声明但没有提供实现的虚函数。
纯虚函数通过在基类中使用 virtual 关键字并在函数声明末尾添加 = 0 来声明。
纯虚函数的目的是定义一个接口或协议,要求派生类必须提供对应的实现。
拥有纯虚函数的类被称为抽象基类,它们无法实例化对象。
派生类必须实现基类中的纯虚函数,否则派生类也将成为抽象类。
```cpp
/*
抽象函数(abstract function)是一种在父类中声明但没有提供实现的函数。
抽象函数用于定义接口或约定,要求子类必须实现这些函数。抽象函数通过将其声明为纯虚函数来实现。
以下是抽象函数的特点和用法:
声明抽象函数:
在基类中,可以通过在函数声明后面添加 = 0 来声明纯虚函数,即不为它提供实现。
基类中至少包含一个纯虚函数时,该基类被称为抽象基类。
派生类实现:
派生类必须实现基类中的纯虚函数,否则派生类也将成为抽象类,无法实例化对象。
派生类必须提供纯虚函数的实现,可以在派生类中重新声明纯虚函数,并提供相应的实现。
接口定义:
抽象函数用于定义接口或约定,指定派生类必须提供的功能。
派生类可以根据需要实现抽象函数,以满足基类定义的接口要求。
动态绑定:
通过基类指针或引用调用抽象函数时,会在运行时根据指针或引用指向的对象的实际类型来调用派生类的实现。
动态绑定实现了多态性,允许基类指针或引用在不同的派生类对象上调用适当的函数。
抽象函数允许定义基类的接口并要求派生类实现特定的功能。它们提供了一种机制,可以将类的继承关系用于定义多个相关类之间的公共行为和接口。抽象函数使得代码更具灵活性,能够根据实际需求定义不同的派生类,并通过基类指针或引用来操作这些派生类对象。
*/
#include <iostream>
class Shape {
public:
//基础类定义一个虚函数,等待子类实现
//纯虚函数,抽象函数,此处的const可有可无
virtual void draw() const =0;
void display() const{
std::cout<<"调用display"<<std::endl;
}
};
class Rectangle :public Shape {
public:
void draw()const {
std::cout << "调用Rectangle draw" << std::endl;
}
};
class Circle: public Shape {
public:
void draw()const {
std::cout << "调用Circle draw" << std::endl;
}
};
int main() {
Circle *cir = new Circle();
cir->draw();
cir->display();
delete cir;
return 0;
}
抽象类
抽象类的主要特点包括:
1.抽象类不能被实例化,可以用来定义指针,即不能创建抽象类的对象。
2.抽象类可以包含成员变量、成员函数(包括非虚函数)、静态成员等。
3.抽象类必须至少包含一个纯虚函数,通过纯虚函数定义接口或协议。
4.派生类必须实现抽象类中的所有纯虚函数,才能成为具体的类,否则也是抽象类
动态绑定原理:虚表
虚表(vtable)是一种用于实现多态性的机制。它是编译器为每个包含虚函数的类生成的一张表,用于存储虚函数的地址。虚表允许在运行时通过基类指针或引用来调用派生类的特定虚函数。
虚表通常是一个包含函数指针的数组,每个函数指针指向一个虚函数的地址。对于每个具有虚函数的类,编译器会在该类的对象中添加一个指向虚表的指针,通常称为虚指针(vptr)。虚指针指向虚表,使得在运行时可以根据对象的实际类型来访问正确的虚函数。
当通过基类指针或引用调用虚函数时,编译器会通过虚指针找到该对象对应的虚表,并根据函数的索引或偏移量来调用正确的虚函数。这种方式称为动态绑定,它使得在运行时根据对象的实际类型来确定调用的函数。
虚表的使用使得 C++ 实现了运行时多态性,允许在基类指针或引用上执行派生类的特定函数。这是面向对象编程中的重要概念,使得代码可以更灵活和可扩展。虚表的维护和调用是由编译器和运行时系统自动处理的,开发者无需手动管理虚表的创建和使用。
#include <iostream>
class Base {
public:
virtual void foo() {
std::cout << "Base::foo() called" << std::endl;
}
virtual void bar() {
std::cout << "Base::bar() called" << std::endl;
}
};
class Derived : public Base {
public:
void foo() override {
std::cout << "Derived::foo() called" << std::endl;
}
void bar() override {
std::cout << "Derived::bar() called" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->foo(); // 动态绑定,调用 Derived::foo()
ptr->bar(); // 动态绑定,调用 Derived::bar()
delete ptr;
return 0;
}
继承
允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和行为。通过继承,派生类可以获得基类的成员变量和成员函数,并可以在此基础上添加自己的特定实现或扩展。
class 派生类 : 继承类型 基类{
}
1.继承是继承父类德全部成员(不包括构造和析构函数)不管是否私有公开,但是权限控制无法随意访问
2.派生类如果和基类有同名的成员,那就需要类名::成员来调用父类的成员
3.派生对象是基类的对象,基类对象不一定是派生类的对象.
子类访问父类成员
#include <iostream>
using namespace std;
class Fat {
public:
int age;
int val = 10;
void setAge();
Fat() ;//定义构造函数
Fat(int a);//定义带参构造函数
~Fat();//定义析构
};
void Fat::setAge() { this->age = 11; }
Fat::Fat(int a) { this->age = a; cout << "Fat 带参构造" << endl;}
//子类继承父类
class Son :public Fat {
public:
Son();
~Son();
Son(int a);
int age;
int vall = 1;
void setAge();
};
Fat::Fat() { this->setAge(); cout << "Fat()" << endl;}
Fat::~Fat() { cout << "~Fat()" << endl; }
Son::Son() { this->setAge(); cout << "Son()" << endl;}
Son::~Son() { cout << "~Son()" << endl; }
void Son::setAge() { this->age = 110; }
//*********
//子类实现带参构造
Son::Son(int a) :Fat(a),age(100){ //通过带参初始化fat,使用参数列表初始son参数
//Fat(a);//错误,这样是调用了Fat生成一个匿名对象
}
int main() {
Son b;
cout << "Son:age-" << b.age << endl;
cout << "Son:vall-" << b.vall << endl;
Fat f;
cout << "Fat:age-" << f.age << endl;
cout << "Fat:val-" << f.val << endl;
//访问父类的变量
cout << "Son:Fat age-" << b.Fat::age << endl;
cout << "Son:Fat val-" << b.Fat::val << endl;
cout << "size of " << sizeof(b) << endl;
Son* c;
Fat* d;
//c = &f;语法不通过
c = &b;
d = &b;
//子类对象一定是父类的对象
//子类初始化的时候如果父类是带参构造,如何进行父类初始化?
Fat* e;
e=new Son(20);//调用Son::Son(int a) :Fat(a)这个构造含糊
cout << "Son带参构造后Fat的age"<< e->age<< endl;
}
//先进后出
/*
Fat()//先调用父类的构造函数
Son()//然后调用子类的构造函数
Son:age-110
Son:vall-1
Fat()
Fat:age-11
Fat:val-10
Son:Fat age-11
Son:Fat val-10
size of 16
Fat 带参构造
Son带参构造后Fat的age20
~Fat()//后出现的变量先调用析构,这里的调用的是f变量
~Son()//子类先调用析构子类成员
~Fat()//然后调用父类析构回收对象的父类成员
*/
派生类和基类之间的关系
继承类型
** class的默认继承方式为private,而struct的默认继承方式为public。**
C++ 中的继承可以分为以下几种类型:
1.公有继承(public inheritance):通过 public 关键字声明,基类的公有成员在派生类中仍然是公有的,基类的保护成员在派生类中变为保护的,基类的私有成员在派生类中不可访问。
2.保护继承(protected inheritance):通过 protected 关键字声明,基类的公有和保护成员在派生类中都变为保护的,基类的私有成员在派生类中不可访问。
3.私有继承(private inheritance):通过 private 关键字声明,基类的公有和保护成员在派生类中都变为私有的,基类的私有成员在派生类中不可访问。
![在这里插入图片描述](https://img-blog.csdnimg.cn/88cc0cd93a0345929a50049dd2d7ee32.png)
#include <iostream>
class Shape {
protected:
int width;
int height;
public:
void setWidth(int w) {
width = w;
}
void setHeight(int h) {
height = h;
}
};
class Rectangle : public Shape {
public:
int getArea() {
return width * height;
}
};
class Circle : protected Shape {
public:
int getArea() {
setHeight(10);
setWidth(10);
return width * height;
}
};
class CircleB : protected Circle {
public:
int getArea() {
setHeight(10);//孙子派生类可以访问
setWidth(10);
return width * height;
}
};
int main() {
Rectangle rect;
rect.setWidth(5);//对象自己可以访问
rect.setHeight(10);
int area = rect.getArea();
// 输出矩形的面积
std::cout << "Rectangle Area: " << area << std::endl;
Circle circ;
//circ.setWidth(5);//不可访问,因为此类非子类不能调用
//circ.setHeight(10); //不可访问
int cirarea = circ.getArea();
// 输出矩形的面积
std::cout << "Circle Area: " << cirarea << std::endl;
CircleB circb;
//circ.setWidth(5);//不可访问
//circ.setHeight(10); //不可访问
int cirareab = circb.getArea();
// 输出矩形的面积
std::cout << "CircleB Area: " << cirareab << std::endl;
return 0;
}
多继承
一个派生类可能会有多个需要继承的基类,例如男人这个类需要继承人,雄性这两个类,这时候就需要多继承
派生类同时继承了多个基类的成员变量和成员函数。
每个基类通过其自己的访问修饰符指定了在派生类中的访问级别。
class 派生类 : 继承类型1 基类1,继承类型2 基类2{
}
菱形继承
多个继承就会引发一个问题:如果多个基类的基类又是同一个类,这种继承就会引发一些潜在的问题
多继承引发菱形继承,解决方案使用虚继承
Base
/ \
A B
\ /
Derived
在上面的示意图中,Base 是一个基类,A 和 B 是派生类,它们分别通过单独的路径继承自 Base。然后 Derived 类通过多继承同时继承了 A 和 B。
菱形继承导致的问题:
1.冗余数据:由于 Derived 继承了两个路径上的 Base,所以在 Derived 对象中会存在两份 Base 类的成员变量。这导致了内存空间的浪费和数据冗余。
2.名称冲突:如果 A 和 B 中定义了相同名称的成员变量或成员函数,那么在 Derived 中使用这些名称时会产生冲突。编译器无法确定使用哪个继承路径上的成员,从而导致编译错误。
3.虚函数冲突:如果 A 和 B 中定义了相同的虚函数,并且 Derived 没有重写这个虚函数,那么在调用这个虚函数时会产生二义性。编译器无法确定使用哪个继承路径上的虚函数实现。(因为都是父类,而且都有实现)
为了解决菱形继承问题,C++ 提供了虚继承(virtual inheritance)机制。通过在派生类对基类的继承声明中使用 virtual 关键字,可以确保只有一个实例的基类成员。虚继承可以避免这些问题.
问题:如果多个基类之间实现函数重写一个方法,派生类不生成对应的方法会发生什么?
直接报错不明确,必须要在子类进行额外重写
虚继承
#include <iostream>
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Mammal : virtual public Animal {
public:
void makeSound() {
std::cout << "Mammal makes a sound." << std::endl;
}
};
class Bird : virtual public Animal {
public:
void makeSound() {
std::cout << "Bird makes a sound." << std::endl;
}
};
class Platypus : public Mammal, public Bird {
public:
void makeSound() {//必须重写函数,可以在函数中明确如何调用
Bird::makeSound();
}
};
int main() {
Platypus platypus;
platypus.makeSound(); // 输出:"Bird makes a sound."
return 0;
}
常量数据成员和常量函数成员
友元函数
友元关系可以在类定义中声明,被声明为友元的类或函数可以直接访问另一个类的私有成员,而无需通过公有或受保护的接口。友元关系是单向的,即友元关系不具有传递性。不继承,可以定义多个,。友元关系可以提供更灵活的访问权限控制,
友元类
final
权限掠夺者:
1.可以掠夺函数权限,阻止重写
2.掠夺类的权限,组织派生
函数使用
#include <iostream>
using namespace std;
class Fat {
public:
Fat();
virtual ~Fat();
virtual void test_func();
};
class Son :public Fat {
public:
Son();
~Son();
void test_func() final;//必须是虚函数才能用final修饰,不允许派生类重写此方法
};
void Son::test_func() {
cout << "Son:test_func" << endl;
};
Fat::Fat() { cout << "Fat()" << endl; }
Fat::~Fat() { cout << "~Fat()" << endl; }
Son::Son() { cout << "Son()" << endl; }
Son::~Son() { cout << "~Son()" << endl; }
class Son2 :public Son {
};
void Fat::test_func() {
cout << "Son:test_func" << endl;
};
int main() {
Son obj;
Son2 obj2;
}
类使用final修饰
如果finnal修饰纯虚方法,子类无法实现会怎么样?
virtual void test_func2() final =0 ;
操作符重载
联编
参考
https://blog.csdn.net/weixin_60053402/article/details/129633983
运算符重载
示例
#include <iostream>
/*
* 运算符重载,重新定义运算符的运算规则
* //返回值类型 函数名(形参列表){}
* 函数名:operator运算符
*
*/
using namespace std;
class MyComplex {
public:
MyComplex(double real=0.0,double image=0.0);
~MyComplex();
void display();
//类内重载
MyComplex operator+(const MyComplex& other) const;
private:
double m_real;
double m_image;
};
MyComplex MyComplex:: operator+(const MyComplex& other) const {
cout << "!" << other.m_image << endl;
return MyComplex(this->m_real + other.m_real, this->m_image + other.m_image);
};
MyComplex::MyComplex(double real,double image):m_real(real),m_image(image) {}
void MyComplex::display() {
cout << "(" << this->m_real <<"+" << this->m_image << "i)";
}
MyComplex::~MyComplex() {}
int main() {
MyComplex com1(12.23,23.43);
MyComplex com2(12.4,34.2);
com1.display();
com2.display();
MyComplex com3 = com1 + com2;
com3.display();
com1.operator+(com2);
com2.operator+(com1);
//后面的值是other,this是左边的值
}
注意:
不能有默认的参数
并不是所有的运算符都能重载
sizeof不能重载,三目运算符不能重载,成员运算符(点),作用域运算符
不能改变结合性和优先级
运算符重载函数可以是类的成员函数,也可以作为全局函数
箭头运算符->,下标运算符[],赋值运算符=,函数调用运算符()
只能以成员函数的形式重载