三、指针、数组、函数
1.什么是指针?
指针其实也是个变量,只不过这个变量里面存储的是内存地址。
2.什么是指针的类型?
举个例子:
int * a;指针类型为int*
char * c;指针类型为char*
3.什么是指针所指向类型
举个例子:
int * a;指针指向类型为int
char * c;指针指向类型为char
注意区别
4.指针的值,或者叫指针所指向的内存区或地址
指针是一个变量,它存储的是另一个变量的内存地址。通过指针,我们可以直接访问和操作该内存地址中的数据。
1、指针的值:指针的值是指针变量本身所存储的内容,这个内容是一个内存地址。这个地址指向了某个内存区域,该内存区域可能存储了某种类型的数据。
2、指针所指向的内存区或地址:当我们说指针指向某个内存地址时,我们是指该指针变量的值(即它所存储的内存地址)对应的那个内存区域。这个内存区域可以存储任何类型的数据,具体取决于我们如何使用这个指针。
5.指针本身所占据的内存区,函数指针
在32位系统中指针是4个字节,64位系统中指针是8个字节
函数指针:首先是个指针,这个指针指向函数
应用场景:回调
例如:
int (*fun) (int, int );
实例:
int max(int x, int y)
{
int z;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;
}
int main()
{
int (*p) (int, int);//定义一个函数指针
int a, b, c;
p = max; //把函数的地址给指针
a = 20;
b = 30;
c = (*p)(a, b);//调用函数指针
printf("%d\n", c);
return 0;
}
6.指针的运算
例子1:
int *ptr; //假设指针指向的地址是0x 00 00 00 00
ptr++; //运算之后指针指向0x 00 00 00 08
// 注意p+1也是加4个字节,因为int在64位或32位系统中是一样占用4字节的,运算的时候是以单元为单位及一个int,即4个字节
char *p;
p++; //地址偏移1
// 注意:对于一级指针的++操作偏移量要看指针指向的是什么类型,对应二级指针偏移量,对于64系统是8个字节,因为二级指针指向的类型就是指针,所以永远是一个指针类型的大小
例子2:
#include <stdio.h>
int main()
{
char a[20] = "You_are_good";
char *p = a; char **ptr = &p;
printf("**ptr = %c\n", **ptr);
ptr++;
printf("**ptr = %c\n", **ptr);
}
在这个例子中是无法确定二级指针++之后指向的地址内容,因为二级指针(ptr)指针指向的一级指针的地址,如果二级指针(ptr)++之后,那么就会指向一级指针的后8个字节(对于64位操作系统来说指针类型是8字节),至于这个地址里面是啥无从得知
7.指针数组
首先是个数组,这个数组里的元素是指针
例如:
int *p[4];
int a[4] = {1,2,3,4};
p[0] = &a[0];
printf("%d\n", *p[0]);
8.数组指针和一维数组
首先是个指针,这个指针指向数组例如:
int (*p)[4]; //表示一个指向有4个int 元素的数组的指针,所以p+1,加的是4个int
int num[8] ={1,2,3,4,5,6,7,8};
int (*p)[4];
p = num;
printf("%d\n", sizeof(p));//8 因为p是指针,64位系统8个字节
printf("%p\n", p);//0x7ffe11d8a4e0
printf("%p\n", p + 1);//0x7ffe11d8a4f0 //加的是指针指向的类型大小,这里指针指向的是有四个4int元素的数组,所以加的是16个字节
如何求某个元素?
比如求num[5]的元素
表示如下: * (*(p+1)+1)
其实你就把数组指针看成是二维数组就行,p+1,加的是一个一维数组的长度,(p+1)就是二维变一维组数,((p+1)+1)表示后移一个地址,((p+1)+1)取出元素。
9.指针函数
首先是个函数,这个函数的返回值是个指针类型
例如:
int * fun(int x, int y){}
10.指针和数组的区别?(类型、赋值、内存、字节大小、修改内容)
1.概念
数组:是同种类型的集合
指针:里面保存的地址的值
2.赋值:
同种类型指针之间可以直接赋值,数组只能一个个元素赋值
3.存储方式:
数组是连续的一段空间,指针的存储空间是不确定的
4.修改内容不同
比如:
char p[] =”hello”,我们可以执行p[0] = ‘s’操作原因是p是数组,可以使用下标的方式进行修改数组内容
char * p = “hello” 执行p[0] = ‘s’是错误的,原因是p是指针,指向字符串常量,常量是不允许修改的
5.所占字节不同
指针在64位系统中是8字节的,而数组是不固定的,要看数组的类型和元素个数
11.什么是野指针?如何产生?如何避免?
野指针:是指指针指向的地址是不确定的
原因:释放内存之后,指针没有及时置空
避免:
-
初始化置NULL
-
申请内存后判空
-
指针释放后置NULL
-
使用智能指针
一般流程如下:
assert函数:
void assert(int expression);
expression – 这可以是一个变量或任何 C 表达式。如果 expression 为 TRUE,assert() 不执行任何动作。如果 expression 为 FALSE,assert() 会在标准错误 stderr 上显示错误消息,并中止程序执行。
int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0
assert(p1 != NULL); //判空,防错设计
free(p1);
p1 = NULL; //释放后置空
12.什么是智能指针
智能指针是个类,用来存储指针(指向动态分配对象的指针)
C++程序中使用堆内存是非常频繁的,堆内存的申请和释放由程序员手动管理,这很容易造成堆内存的泄漏,使用智能指针能更好的管理堆内存
13.智能指针的内存泄漏问题是如何解决的?
为了解决循环引用导致的内存泄漏,引入了weak_ptr
14.数组名num /&num的区别
对于一维数组来说
num+1是偏移到下个元素,&num+1是偏移整个数组
对于二维数组来说
num+1是偏移一个一维数组,&num+1是整个数组
15.有了指针为什么还需要引用
直接的原因是为了支持运算符重载
用指针的使用经常犯得错:
1,操作空指针,2,操作野指针,3,不知不觉改变了指针的值,而后还以为该指针正常。如果我们要正确的使用指针,我们不得不人为地保证这三个条件。而引用的提出就解决了这个问题。
引用区别于指针的特性是 :1,不存在空引用(保证不操作空指针),2,必须初始化(保证不是野指针),3,一个引用永远指向他初始化的那个对象(保证指针值不变)
16.使用指针的好处
1.指针可以动态分配内存
2.在链表中可以方便修改链表的节点
3.解析字符串
4.相同类型的指针可以直接复制
17.指针常量、常量指针、指向常量的常量指针
const int* p
和int const *p
是指向常量的指针,可以改变指针指向的地址,但不能通过指针修改指向的值。int* const p
是指针常量,不能改变指针指向的地址,但可以通过指针修改指向的值。const int* const p
是指向常量的指针常量,既不能改变指针指向的地址,也不能通过指针来修改它所指向的值。
18.指针和引用的异同,如何转换
1.都是指针的概念,指针保存的是内存的地址,引用是某块内存的别名,这个内存一但初始化就不能再去指向别的内存
2.两者都会占用内存
区别:
-
指针是实体,而引用是别名
-
引用的本质是指针常量,指向指针的地址不可以改变,指向地址的内容可以改变
-
自增表示的意义不同,指针自增表示地址自增,引用表示值自增
-
引用必须初始化
-
引用不能为空
-
sizeof(引用)得到的是所指向的变量的大小,sizeof(指针)的到的是指针大小
-
引用不需要解引用
19.sizeof(数组名)和sizeof(&数组)
int Num[100];
printf("%ld\n", sizeof(Num)); //400
printf("%ld\n", sizeof(&Num)); //8起始就是打印int *指针大小,64位操作系统
printf("%ld\n", sizeof(int *)); //8
20.二维数组
int a[3][3];
1.int a[3][3];表示是个三行三列的二维数组
2.数组名表示数组首元素的地址,即第0行第0个地址
3.a+1表示地址偏移一个一维数组的地址,即三列int大小=34 = 12
4.*a 表示去二维变一维,*a就相当于一维数组的数组名,比如 *a +1 表示第0行下标为1的元素地址,只是偏移一个Int地址
21.指针减指针
地址相减 = (地址a -地址b)/sizeof(指针指向的类型)
int a[3];
a[0] = 0;
a[1] = 1;
a[2] = 2;
int *p, *q;
p = a;
q = &a[2];
printf("%p\n", p);//0x7ffe80e38b0c
printf("%p\n", q);//0x7ffe80e38b14
printf("%d\n", q - p);//2 ( q - p) /sizeof(int )
// 那么就有 a[q-p] = a[2] = 2
22.数组作为参数传递
//数组名作为参数传递时,实际上是变为指针,所以,sizeof(arr)是指针的大小
void print_array(int arr[]) {
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);// 1 2
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
print_array(arr);
return 0;
}
23. 数组初始化
对于二维数组可以
int num[][10] 第一个【】可以不填,但是第二个必须填,但是这个数组必须初始化
对于一维数组
int num[] 这【】可以不填但是这个数组必须初始化
总结:
数组的最左边的[]可以不填,这个时候数组必须初始化,二维数组的第二个[]必须填,不管有没有初始化
int a[][2]; //不允许
int b[][2]={1, 2, 3, 4};//可以
int c[] = {1, 2, 3};//可以
int c[]; //不可以
int d[][]//不允许,第二个[]必须填,不管有没有初始化
对于二维数组来说,第一维就是最左边的[],也就是行数
24.调用free释放内存后,指针还能用吗
free释放掉内存后,只是把内存的使用权就被归还给系统,内存里面的东西可能被清除也可能是垃圾值,但是指向这个内存的指针还是指向这块内存,并不会NULL
25.为什么C++默认的析构函数不是虚函数
当类中有虚成员函数时,类会自动生成虚函数表和虚表指针,虚表指针指向虚函数表。每个类都有自己的虚函数表,虚函数表的作用就是保存本类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是各个虚函数的地址。这样一来,就会占用额外的内存,当们定义的类不被其他类继承时,这种内存开销无疑是浪费的
26.静态函数和虚函数的区别?
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定,虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
27.重载和重写(覆盖)
重写:
是指派生类中存在重写函数,函数名,参数,返回值类型必须和基类中被重写的函数一样,只是它们的函数体不一样,被重写的函数必须用virtual修饰
例如:
class A {
public:
virtual void fun(int a) {
cout << “this A ”; }
};
class B: public A {
public:
void fun(int a) {
cout << “this B ”;}
}
派生类对象调用时会调用派生类的重写函数,不会调用被重写函数
重载:
是指函数名相同,函数参数不同,不关心返回值类型
函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
28.虚函数表具体怎么实现运行是多态?
主要是通过虚函数表:首先虚函数表是一个类的虚函数的地址,每个对象在创建时,都会有一个指针指向该类的虚函数表,每一个类的虚函数表按照函数声明的顺序,会将函数地址存在虚函数表里,当子类对象重写父类的虚函数时,父类的虚函数表中对应的虚函数的地址就会被子类的虚函数地址覆盖
29.构造函数有几种,分别什么作用
默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数
1.默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作
有了有参的构造,编译器就不提供默认的构造函数
例如:
class Student{
public:
//默认构造函数
Student() {num=1001; age=18; }
//初始化构造函数
Student(int n, int a):num(n),age(a){}
private:
int num;
int age;
};
int main(){
//用默认构造函数初始化对象S1
Student s1;
//用初始化构造函数初始化对象S2
Student s2(1002, 18);
return 0;
}
2.拷贝构造函数
#include "stdafx.h"
#include "iostream.h"
class Test
{
int i;
int *p;
public:
Test(int ai,int value)
{
i = ai;
p = new int(value);
}
~Test()
{
delete p;
}
Test(const Test& t) //拷贝构造
{
this->i = t.i;
this->p = new int(*t.p);
}
};
// 复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
Test t1(1, 2);
Test t2(t1); // 将对象t1复制给t2。注意复制和赋值的概念不同
return 0;
}
3.移动构造函数。用于将其他类型的变量,隐式转换为本类对象
30.只定义析构函数,会自动生成哪些构造函数
编译器会自动生成拷贝构造函数和默认构造函数
31.说说一个类,默认会生成哪些函数
无参构造函数,拷贝构造函数,赋值运算符,析构函数(非虚)
32.说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
父类构造函数–>成员类对象构造函数–>自身构造函数
33.fork, wait, exec函数
父进程通过fork函数创建一个子进程,此时这个子进程知识拷贝了父进程的页表,两个进程都读同一个内存,exec函数可以加载一个elf文件去替换父进程,从此子进程就可以运行不同的程序,父进程wait函数之后会阻塞,直到子进程状态发生改变。
34.select, epoll, poll 函数区别
1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。
而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列),这也能节省不少的开销。
35. 字符输入函数
fputc、putc、putchar返回字符,puts、fputs返回非负数
36. 文件位置函数
ftell() 函数用于得到文件位置指针当前位置相对于文件首的偏移字节数;
fseek()函数用于设置文件指针的位置;
rewind()函数用于将文件内部的位置指针重新指向一个流(数据流/文件)的开头;
ferror()函数可以用于检查调用输入输出函数时出现的错误。
标签:函数,指向,int,嵌入式,数组,构造函数,指针 From: https://blog.csdn.net/qq_43818724/article/details/137581743