c++面试题
摘自:https://blog.csdn.net/Destiny_zc/article/details/118532083
文章目录
a.0 库
a.0.最小cpp系统构成
a.1. 不使用用自定义库文件
a.2.使用库文件
a.2.1 生成静态库
a.2.2 生成共享库
a.3.使用第三库
a.3.1 使用第三库-共享链接库的使用
a.3.2 使用第三库-静态库的使用
1.1.c++域操作符:
1.2.i++与++i的区别
1.3.++i与i++那个效率高:
1.4.有符号变量与无符号变量的值转换
1.5.不使用任何中间变量,交换a与b的值
1.6.C与C++有什么不同
1.7.如何理解C++是面向对象化的,而C是面向过程化的
1.8.C++中main函数执行完后还会执行其他语句吗
2.1 宏定义,编译与链接
2.2 用#define 实现宏并求最大值和最小值
2.3 宏定义易错点
2.4宏参数的连接
2.5 用宏定义得到一个数组所含的元素个数
2.6 找错-const
2.7 #define与const的区别
2.8 C++ const有什么作用
2.8 static 的作用
2.9 static 全局变量与普通的全局变量有什么区别
2.10 c++静态成员
2.11 sizeof 普通变量
2.12 sizeof 计算类对象大小
2.13 sizeof 含有虚函数的类对象的空间大小
2.14 sizeof 计算虚继承的空间大小
2.16 sizeof()与strlen()区别==>strlen 在C中定义,在C++中没有;
2.17 sizeof 求联合体的大小
2.17 \#pragma pack
2.18 内联函数
内联函数的优点:
内联函数的缺点:
2.19 内联函数与宏的区别
3.1 一般引用
3.2 指针变量的引用;
3.3 变量引用
3.4 参数引用
3.5 引用错误
3.6 指针和引用的区别:
3.7 传引用比传指针安全:
3.8 指针数组与数组指针
3.9 指针加1
3.10 相同内容的数据存储地址关系
3.11 内存越界
3.12 指针常量与常量指针
3.13 this指针
3.14 函数指针与指针函数
3.15 typedef 用于函数指针
3.16 野指针
3.17 有了malloc/free 为什么还需要new/delete
3.17 内存越界
3.18 动态内存传递
4.0 易错
4.0.1 string 与char *的区别
4.1 不使用库函数将数字转换成char
4.2 字符转数字
4.3 strcpy()的实现
4.4 寻找子串
4.5 常用c字符串操作
5.位制转换
6.1 private 数据类型
6.2 初始化的坑
6.3 析构函数与构造函数是否可以被重载
6.4 构造函数中explict与普通函数的区别
6.5 explicit 的作用
6.6 继承类析构方式
7.类的继承
7.1 继承中构造函数调用方式
7.2 虚函数与纯虚函数的区别
7.2.1 虚函数
7.2.2 纯虚函数
8.1 STL
8.2 STL 删除某一个元素后,会自动移项
8.3 list 与vector的区别
a.0 库
a.0.最小cpp系统构成
最小cpp系统构成,只需要一个CMakeLists.txt文件和一个main.cpp;
//CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(addlib)
add_executable(test main.cpp)
//所以最小cpp系统只需要三句cmake,如果需要添加自定义的头文件,和库文件,则需要将其包含在add_executable();此部分见##1.
1
2
3
4
5
6
7
a.1. 不使用用自定义库文件
在Linux中需要使用CMake进行系统文件的构建,如果不生成自定库文件时,需要将所有源文件都添加到cmake的add_executable()中,否则编译出错,只能执行main.cpp,调用其他自定义的.h和.cpp中的函数,都会出现编译错误;例如,现有一下源文件:
//main.cpp 主程序
#include<iostream>
#include"MyHead.h"
using namespace std;
int main()
{
myprint("head test");
return 0;
}
//Myhead.hpp
#ifndef MYHEAD_H
#define MYHEAD_H
#include<string>
#include<iostream>
void myprint(std::string str);
#endif
//Myhead.cpp
#include"MyHead.h"
void myprint(std::string str)
{
std::cout<<"this my head test"<<std::endl;
std::cout<<"content is :"<<str<<std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
此时需要在将新添加的Myhead.hpp与MyHead.cpp,需要在CMakeList.txt文件中add_executable()包含进去,告诉编译器,有两个个可执行文件,分别为main.cpp,MyHead.cpp;否则,编译无法知道Myhead.hpp中的void myprint(std::string str);是如何执行的,从而导致编译出错;
所以此时的CMakeList.txt如下:
cmake_minimum_required(VERSION 3.0)
project(addlib)
add_executable(test main.cpp MyHead.cpp) ## 将MyHead.cpp也添加为可执行文件
## main.cpp 中的myprint()是从MyHead.cpp中解析;
1
2
3
4
a.2.使用库文件
通常我们使用的第三方库都是非开源的,我们只要获得第三方库的库文件和头文件,就可以使用;而库文件,就是将.cpp文件转换成二进制文件,所以此时,我们是无法知道头文件中的某个函数是如何定义,即这个函数的实现方法,即所有库都是一些函数打包的集合;为了使用这个库,我们需要知道这个库里面有什么东西,所以此时需要一本说明,而这个说明就是头文件.所以为了使用第三方库,我们至少需要两个文件库文件和头文件,本节目的就是实现生成一个库文件和使用一个库文件;
在Linux中库文件分为静态库和共享库;此处我们只讨论linux下的静态库与共享库;
静态库是以.a作为后缀名,共享库是.so为后缀的,所有库都是一些函数打包的集合,差别在于静态库每次调用都会生成一个副本,而共享库则只生成一个副本.
a.2.1 生成静态库
我们可以将自定义的源程序生成库文件,此时,我们就可以不用将这个源程序添加到CMakeLists.txt中的add_executable(),为了使用这个库文件(即MyHead.cpp生成的二进制代码),我们需要在CMakeList.txt中新添加一句命令用来将这个库文件和我们的main.cpp链接起来;
我们只需要改变CMakeLists.txt为:
cmake_minimum_required(VERSION 3.0)
project(addlib)
add_executable(test main.cpp)
add_library(mylib MyHead.cpp) ## 将MyHead.cpp打包生成静态库,即生成.a文件
target_link_libraries(test mylib) ## 将可执行程序链接到该库上,此时main的myprint()函数才能执行
## 此时该函数是从生成的.a文件中解析myprint()
1
2
3
4
5
6
7
a.2.2 生成共享库
生成共享库的方法和静态库的方法类似,只需要在add_library()添加关键字SHARED:
cmake_minimum_required(VERSION 3.0)
project(addlib)
add_executable(test main.cpp)
add_library(mylib SHARED MyHead.cpp) ## 将MyHead.cpp打包生成共享库,即.so
target_link_libraries(test mylib) ##将可执行程序链接到该库上,此时main的myprint()函数才能执行
1
2
3
4
5
6
a.3.使用第三库
由前面知道,为了使用第三库,我们需要获取其库文件的和头文件,此处,我们使用自己生成的库文件和头文件;
为了使用第三方库,我们需要指定第三库库文件与头文件对应的位置,最后将源文件与库文件进行链接
a.3.1 使用第三库-共享链接库的使用
//CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(testlib)
#设置第三库的头文件路径与库文件路径
set(INCLUDE_DIRECTORIES ${PROJECT_SOURCE_DIR}/include)
set(LIBRARY_PATH_MY ${PROJECT_SOURCE_DIR}/lib)
message("incude dir:" ${INCLUDE_DIRECTORIES})
message("lib dir:" ${LIBRARY_PATH_MY})
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#添加库
#对于find_package找不到的外部依赖库,可以用add_library添加
# SHARED表示添加的是动态库==>(共享库)
# STATIC表示添加的是静态库
# IMPORTED表示是引入已经存在的动态库
add_library(mylib SHARED IMPORTED)
#指定所添加依赖库的导入路径
set_target_properties(mylib PROPERTIES IMPORTED_LOCATION ${LIBRARY_PATH_MY}/libmylib.so)
# 添加头文件路径到编译器的头文件搜索路径下,多个路径以空格分隔
include_directories( ${INCLUDE_DIRECTORIES} )
# 添加库文件路径到编译器的库文件搜索路径下,多个路径以空格分隔
link_directories(${LIBRARY_PATH_MY})
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} )
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
##链接库
target_link_libraries(${PROJECT_NAME} mylib)
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//main.cpp
#include<iostream>
#include<MyHead.h>//#include"MyHead.h"两者都可以
using namespace std;
int main()
{
myprint("test my link library");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
a.3.2 使用第三库-静态库的使用
静态库的添加与动态库相同,只需要将下面两条语句进行修改:
add_library(mylib SHARED IMPORTED)==>add_library(mylib STATIC IMPORTED) ##修改关键字:SHARED==>STATIC
set_target_properties(mylib PROPERTIES IMPORTED_LOCATION ${LIBRARY_PATH_MY}/libmylib.so)==>
==> set_target_properties(mylib PROPERTIES IMPORTED_LOCATION ${LIBRARY_PATH_MY}/libmylib.a) ## 指向静态库
1
2
3
4
cmake_minimum_required(VERSION 3.0)
project(testlib)
#设置第三库的头文件路径与库文件路径
set(INCLUDE_DIRECTORIES ${PROJECT_SOURCE_DIR}/include)
set(LIBRARY_PATH_MY ${PROJECT_SOURCE_DIR}/lib)
message("incude dir:" ${INCLUDE_DIRECTORIES})
message("lib dir:" ${LIBRARY_PATH_MY})
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#添加库
#对于find_package找不到的外部依赖库,可以用add_library添加
# SHARED表示添加的是动态库==>(共享库)
# STATIC表示添加的是静态库
# IMPORTED表示是引入已经存在的静态库
add_library(mylib STATIC IMPORTED)
#指定所添加依赖库的导入路径
set_target_properties(mylib PROPERTIES IMPORTED_LOCATION ${LIBRARY_PATH_MY}/libmylib.a)
# 添加头文件路径到编译器的头文件搜索路径下,多个路径以空格分隔
include_directories( ${INCLUDE_DIRECTORIES} )
# 添加库文件路径到编译器的库文件搜索路径下,多个路径以空格分隔
link_directories(${LIBRARY_PATH_MY})
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} )
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
##链接库
target_link_libraries(${PROJECT_NAME} mylib)
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1.1.c++域操作符:
#include<iostream>
using namespace std;
int value=0;
void printvalue()
{
cout<<value<<endl;
}
int main()
{
int value=10;
cout<<value<<endl;
/*在c++中可以通过域操作符"::",来直接操作全局变量;但在c中,不支持这个操作,因此会报错*/
::value=100;//修改全局变量的值;
printvalue();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.2.i++与++i的区别
#include<iostream>
using namespace std;
int main()
{
int i=8;
cout<<++i<<endl;//i先自增,再打印i的值;->9
cout<<--i<<endl;//i先自减,再打印i的值;->8
cout<<i++<<endl;//先打印i的值,再自增;->8
cout<<i--<<endl;
cout<<-i++<<endl;//这里"-"表示负号运算,因此先打印-i的值,再i自增1;->-8
cout<<-i--<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
1.3.++i与i++那个效率高:
对于内建数据类型的情况下,效率没有区别,因为两者产生汇编代码基本是一致的;
i++;
//mov eax, DWORD PTR _i$[ebp]
//add eax,1
//mov DWORD PTR _i$[ebp],eax
++i;
//mov ecx, DWORD PTR _i$[ebp]
//add ecx,1
//mov DWORD PTR _i$[ebp],ecx
1
2
3
4
5
6
7
8
自定义数据情况(类),++i的效率较高;
对于自定义数据类型,因为前缀式(++i)可以返回对象的引用,而后缀式(i++)必须返回对象的值,所以导致在大对象的时候,产生较大的复制的开销,引起效率降低;
1.4.有符号变量与无符号变量的值转换
#include<iostream>
#include <limits.h>
using namespace std;
int main()
{
unsigned int i=0; //注意这里i是一个无符号值;
//最小值是0;
cout<<"unsigned int max:"<<UINT_MAX<<endl;//MAX:4294967295
cout<<i<<endl;//->0
i=-1;//比0小,超出unsigned int范围,返回一个较大值->UINT_MAX-|-1+1|=UINT_MAX,相当于,UINT_MAX的倒数第一个值;->4294967295
cout<<i<<endl;
i=-3;//同理,UINT_MAX-|-3+1|=UINT_MAX-2=4294967293;
cout<<i<<endl;
//------------------------------------------------------------------------------------------------------------
char c;
c=100;//给char 类型赋值一个int类型,相当于将ASCII码为100对应的字符赋值给c;
cout<<c<<endl;//->d的ASCII=100
printf("%d\n",c);//->100,其中%d相当于强制转换.等价与->cout<<(int)c<<endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.5.不使用任何中间变量,交换a与b的值
#include<iostream>
using namespace std;
int swap1(int &x,int &y)
{//采取中间变量法
int tmp=x;
x=y;
y=tmp;
}
int swap2(int &x,int &y)
{//采用加减法,x+y,x-y可能导致,数据溢出
x=x+y;
y=x-y;
x=x-y;
}
int swap3(int &x,int &y)
{//采用异或算法;推荐使用
//异或:两元素相同为0,不同为1;
x^=y;
y^=x;
x^=y;
}
int main()
{
int x=2,y=3;
swap1(x,y);
cout<<x<<"\t"<<y<<endl;//->3 2
swap2(x,y);
cout<<x<<"\t"<<y<<endl;//->2 3;前面已经交换了一次
swap3(x,y);
cout<<x<<"\t"<<y<<endl;//->3 2
//x=2==>010,y=3==>011
//x^y=001==>x;
//y^x=010==>y;y=2;
//x^y=011==>x;x=3;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
1.6.C与C++有什么不同
C是一个结构化语言,它的重点在于算法和数据结构,对于语言本身而言,C是C++的子集,C++是对C的扩充,引入了重载,内联函数,异常处理等.
1.7.如何理解C++是面向对象化的,而C是面向过程化的
C是面向过程化的,但是C++不是完全面向对象化的.在C++中也可以写出C一样的过程化的程序,所以只能说C++拥有面向对象的特性;
1.8.C++中main函数执行完后还会执行其他语句吗
如果定义了类的化,会调用析构函数,进行资源释放;
执行atexit()注册的函数.表示函数正常结束时,要被调用的函数;
atexit()函数:函数的参数是一个函数指针,函数指针指向一个没有参数也没有返回值的函数;
一个程序中最多可以用atexit()函数注册32个处理函数,且这些处理函数的调用顺序与其注册顺序相反;
#include<iostream>
using namespace std;
//需要注册的函数没有返回值,也没有参数
void exit1()
{
std::cout<<"exit1..."<<endl;
}
void exit2()
{
std::cout<<"exit2..."<<endl;
}
int main()
{
std::atexit(exit1);//先注册的后执行
std::atexit(exit2);
std::cout<<"主函数执行完毕"<<endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2.1 宏定义,编译与链接
编译:编译的时候检查语言错误,包括关键字对应的语义逻辑,括号匹配,变量和函数存在定义或声明等
链接:链接的时候,就要真正把需要调用的各种变量和函数的二进制代码匹配起来,比如你使用了某个变量,你使用之前声明为外部定义,而实际上你没有给出过实际定义,这是就会报错了。这是所有可执行代码检测的过程。之前编译是每个文件单独变量,生成obj文件。
不带参数宏定义: #define 宏名 字符串:#define PI 3.14 ==>编译时,将所有PI进行替换=3.14,因为替换是在编译之前进行,所以不会对宏名(PI)替换的对象(3.14)进行语法检查,此时的3.14不应该被看作为实数型,而应被看作为一个普通的字符串常量
带参数宏定义 : define 宏名(参数表) 字符串
#define S(a,b) a*b //宏定义中,虽没有定义返回值,但是,是可以通过宏返回运算结果的
...
double result;
result=S(3,2)//预编译结束后,S(3*2)被3*2代替;
//此时,3为a的实际参数,2为b的实际参数,则宏名S(3,2)经过预处理后,用3*2替换后再进行编译.即S(a*b)等同行于a*b,而S(3,2)等同于3*2;
1
2
3
4
5
采用宏定义编译的时候,编译器会"机械"的将所定义的字符进行替换,这种替换可能产生错误;
2.2 用#define 实现宏并求最大值和最小值
#include <iostream>
using namespace std;
#define MAX(x, y) (((x) > (y)) ? (x) : (y)) //可以将结果直接返回
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
int main()
{
int a = 2, b = 3, result;
result = MAX(a, b);
std::cout << result << endl;
result = MIN(a, b);
std::cout << result << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
在宏的定义过程中,由于直接替换可能导致,意外的结果,所以需要用括号,将参数括起来.提高优先级;
2.3 宏定义易错点
#include <iostream>
using namespace std;
#define SQR(x) (x * x)
int main()
{
int a, b = 3;
a = SQR(b + 2);//->输出的是11,而不是25;
std::cout << a << endl;
}
1
2
3
4
5
6
7
8
9
这是由于,宏定义展开是在预处理时期,也就是在编译之前,此时b并没有被赋值,这是b只是一个符号.因此展开为->a=(b+2*b+2);所以,进行计算后,得出11;
改正:
可以将#define SQR(x) (x * x)==>#define SQR(x) ((x) * (x))
2.4宏参数的连接
首先来介绍一下这两种功能:
#的用法是负责将其后面的东西转化为字符串,比如:
#define TO_STRING(str) #str
int main(){
cout << TO_STRING(this is a string) << endl;
return 0;
}
1
2
3
4
5
##是连接符,将前后两个东西连接成一个词。比如:
#define IntVar(i) int_##i
int main(){
int int_1 = 1, int_2 = 2;
cout << IntVar(1) << ", " << IntVar(2) << endl;
return 0;
}
1
2
3
4
5
6
7
#include <iostream>
using namespace std;
/*使用#把宏参数变成一个字符串,用##把两个宏参数贴合在一起*/
#define STR(s) #s
#define CONS(a,b) (int)(a##e##b)
int main()
{
cout<<STR(vck)<<endl;
cout<<CONS(2,3)<<endl;
}
1
2
3
4
5
6
7
8
9
10
2.5 用宏定义得到一个数组所含的元素个数
#include <iostream>
using namespace std;
#define ARR_SIZE(a) (sizeof((a))/sizeof((a[0])))//在编译时,只会将ARR_SIZE(a)替换,此时数组a并不存在,所以需要在使用时,指定数组;
int main()
{
int arry[100];
int size=ARR_SIZE(arry);
cout<<size<<endl;
}
1
2
3
4
5
6
7
8
9
2.6 找错-const
#include <iostream>
using namespace std;
int main()
{
const int x=1;
int b=10;
int c=20;
/*常量指针:,指针指向的内容是不可改变的,指针看起来好像指向了一个常量。*/
/*指针常量:在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化。*/
const int * a1=&b; //常指针,指向常数
int *const a2=&b; //指针常量,不能改变指针
const int *const a3=&b; //指向常量的常指针;
x=2; //x是const类型,所以不能直接修改
a1=&c; //正确
*a1=1;//错误,a1指向是一个常量,不可以被修改
a2=&c;//错误,指针常量只能在定义时初始化,不可以被赋值;
*a2=1;//正确
a3=&c;//错误,指针常量只能在定义时初始化,不可以被赋值;
*a3=1;//错误;a3指向是一个常量,不可以被修改,同时a3也不可以被修改;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2.7 #define与const的区别
#define 只是用来作文本替换;
#define PI 3.1415926
1
当程序进行编译的时候,编译器会首先将"#define PI 3.1415926"以后所有的代码中"PI"用3.141592进行替换,然后进行编译.它的生命期止于编译期,它存在程序的代码段,实际程序中他只是一个常数,一个命令中的参数,并没有实际存在;
const
const 存在与程序的数据段,并在堆栈上分配空间.const 常量是一个run-time类型的,它在程序中确确实实存在,并可以被调用/传递.编译器可以对const常量进行安全检查;
2.8 C++ const有什么作用
const用于定义常量:const用于定义常量,编译器可以对其进行数据静态类型安全检查;
const修饰函数形式的参数;当输入参数为用户自定义类型和抽象数据类型时,应该将值传递改为const &传递,可以提高效率;
void fun(A a);
void fun(A const & a);
1
2
第一个函数效率低,函数体产生的A类型的临时对象用于复制参数a,临时对象的构造,复制,析构过程都将会消耗时间.
第二个函数,用引用传递,不需要产生临时对象,节省了临时对象的构造,复制,析构过程消耗的时间.但是光用引用有可能改变a,所以加const.
const 修饰函数的返回值;如给"指针传递"的函数返回值加const,则返回值不能被直接修改,且返回值只能被赋值给加const修饰的同类型指针;
const修饰类的成员函数:任何不会修改数据成员的函数都应用const修饰,这样,当不小心修改了数据成员调用了非const成员函数,编译器就会报错.
2.8 static 的作用
在模块内(在函数体外),一个被声明的静态变量,可以被模块内所有的函数访问,但不能被模块外其他函数访问,它是一个本地的全局变量;作用域从定义位置起结束;
在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用,这个函数被限制在声明它的模块的本地范围内使用.
2.9 static 全局变量与普通的全局变量有什么区别
存储方式:两者是相同的,均是静态存储,存储在静态存储区.
作用域:不同,非静态全局变量的作用域是整个源程序,当一个程序有多个源文件组成时,非静态全局变量在各个源文件之间都是有效的.而静态全局变量的则限制了其作用域,即只在定义该变量的源文件内有效,在同一个源程序的其它源文件中不能使用它;
tips:static 函数与普通函数的区别,static 函数在内存中只有一份,普通函数在每次调用时,维持一份赋值品;
2.10 c++静态成员
类的静态成员,是类的公共成员,不同对象共享这一个成员;
静态成员,只能通过静态成员函数进行访问,而静态成员函数不仅可以通过类对象进行调用,也可以通过类::静态成员函数进行调用;
#include <iostream>
using namespace std;
class widget
{
private:
static int count;
public:
widget(/* args */);
static int num() //!静态成员,必须通过静态成员函数进行操作;
{
return count;
}
~widget();
};
widget::widget(/* args */)
{
count++;
}
widget::~widget()
{
--count;
}
int widget::count=0;//!静态成员是公共成员变量,必须在类外进行初始化;
int main()
{
widget x,y;
cout<<"widget count:"<<widget::num()<<endl; //==>2
widget z;
cout<<"widget count:"<<widget::num()<<endl; //==>3
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
结果分析:
输出的结果是2,3.由于静态成员数据是公共成员数据,所以共享成员数据,在构造函数中 count++;也就是说,每构造一个类成员,都会导致count++,所以,count相当于记录着类成员的个数;
2.11 sizeof 普通变量
#include <iostream>
using namespace std;
void fun(int arr_[100])
{
cout<<sizeof(arr_)<<endl;//==> 8;//因为在传递时候,传递是数组arr的地址,所以占用8个字节(64位);
}
int main()
{
int arr[100];
char str[] = "Hello";
int *p = arr;
cout << sizeof(arr) << endl;//==>400;此时计算的是一个数组的总共大小;
cout << sizeof(str) << endl;//==>6 :5个字符+'/0'
cout << sizeof(p) << endl; //?==>8 (64位)
/*
无论是什么类型的指针变量,在32位系统下,一个指针变量所占用的空间是4个字节,
在64位下,一个指针变量所占用的空间是8个字节。
64位操作系统下,寻址范围的最大长度为64bit,需要用16个十六进制数。
表示16个十六进制数,需要4乘16bit,即8Byte
*/
fun(arr);//注意传递是数组的首地址,而不整个数组;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2.12 sizeof 计算类对象大小
#include <iostream>
using namespace std;
class A
{
public:
int i;
};
class B
{
public:
char ch;
};
class C
{
public:
int i; //最大基本数据类型:4字节
short j;
};
class D
{
public:
int i; //最大基本数据类型:4字节
short j;
char ch;
};
class E
{
public:
int i;
int ii;
short j;
char ch;
char chr;
};
class F
{
public:
int i;
int ii;
int iii;
short j;
char ch;
char chr;
};
int main()
{
cout << "sizeof(int)=" << sizeof(int) << endl; //==>4
cout << "sizeof(short)=" << sizeof(short) << endl; //==>2
cout << "sizeof(char)=" << sizeof(char) << endl; //==>1
cout << "sizeof(A)=" << sizeof(A) << endl; //==>4
cout << "sizeof(B)=" << sizeof(B) << endl; //==>1
cout << "sizeof(C)=" << sizeof(C) << endl; //==>8
cout << "sizeof(D)=" << sizeof(D) << endl; //==>8
cout << "sizeof(E)=" << sizeof(E) << endl; //==>12
cout << "sizeof(F)=" << sizeof(F) << endl; //==>16
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
易错点:
对于 A B类的大小是比较容易理解的,但是为什么C D E F 输出不是 6 7 12 15?而是 8 8 12 16.这是由于字节对齐引起的.
字节对齐是为了提高读取效率,假如某硬件平台是从偶数字节进行读取,那么如果一个int类型从偶数字节开始存储,那么一个读周期就可以读取,若该int类型从奇数字节开始存储,那么可能需要2个读周期.
所以为了满足字节对齐:
对于C:基本最宽的基本类型为int,大小为4个字节,需要在补三个字节,凑成4的倍数.
所以,字节对齐最终使得,该类占用的字节数为,最大基本类型的 整数倍
2.13 sizeof 含有虚函数的类对象的空间大小
#include <iostream>
using namespace std;
class Base
{
private:
int a;
public:
Base(int x);
void print()
{
cout << "base" << endl;
}
};
Base::Base(int x) : a(x)
{
}
class Derived : public Base
{
private:
int b;
public:
Derived(int x);
void print()
{
cout << "derived" << endl;
}
};
Derived::Derived(int x) : Base(x - 1), b(x)
{
}
class A
{
private:
int a;
public:
A(int x):a(x){}
virtual void print()
{
cout << "A" << endl;
}
};
class B : public A
{
private:
int b;
public:
B(int x):A(x-1),b(x){}
virtual void print()
{
cout << "B" << endl;
}
};
int main()
{
Base obj(1);
cout << "size of Base obj is:" << sizeof(obj) << endl; //==> 4 对于Base类,它占用的内存大小sizeof(int)=4,print()函数不占内存
Derived obj2(2);
cout << "size of Derived obj is:" << sizeof(obj2) << endl; //==>8 比Base 多一个 int ,print()函数不占内存;
A a(1);
cout << "size of A obj is:" << sizeof(a) << endl; //==>16?
//!对于含有虚函数的类,不仅需要给数据成员分配内存,还需要包含一个隐含的虚表指针成员,
//!所以,内存大小应该为:sizeof(int)+指针大小; 应该等于 4+8=12,为什么这里是16?
B b(2);
cout << "size of B obj is:" << sizeof(b) << endl; //==>16 比A多一个int
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
所以可见,普通函数不占用内存,只要有虚函数,就会占用一个指针大小的内存,原因是是系统多用一个指针维护这个类的虚函数表,并且无论多少含有多少个虚函数,都只会产生一个指针.
2.14 sizeof 计算虚继承的空间大小
#include <iostream>
using namespace std;
class A
{
};
class B
{
};
class C : public A, public B
{
};
class D : virtual public A
{
};
class E : virtual public A, virtual public B
{
};
class F
{
public:
int a;
static int b;
};
int F::b = 10;
int main()
{
cout << "sizeof(A)=" << sizeof(A) << endl; //!==>1,因为没有成员数据,所以编译器会安插一个char给空类,用来标记它的每一个对象;
cout << "sizeof(B)=" << sizeof(B) << endl; //==>1,因为没有成员数据,所以编译器会安插一个char给空类,用来标记它的每一个对象;
cout << "sizeof(C)=" << sizeof(C) << endl; //==>1,多继承A B,所以编译器会安插一个char给空类,用来标记它的每一个对象;
cout << "sizeof(D)=" << sizeof(D) << endl; //==>8(4),虚继承A,编译器为该类安插一个指向父类的指针;
cout << "sizeof(E)=" << sizeof(E) << endl; //?==>8,虚继承A,B,此处不应该为16?两个指针,指向两个父类;
cout << "sizeof(F)=" << sizeof(F) << endl; //!==>4,存在一个静态成员,这个静态成员的空间,不存在类的实例中,而像全局变量一样,存储在静态存储区;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2.16 sizeof()与strlen()区别==>strlen 在C中定义,在C++中没有;
sizeof 是操作符,strlen()是函数;
sizeof 返回值类型不一样,sizeof返回值是size_t,strlen()返回值是unsigned int;
sizeof 可以使用多个类型作为参数,strlen()只能使用char *作为参数;
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
int main()
{
char *str = "hello";
cout << sizeof(str) << endl; //==> 8,指针存储空间大小
cout << strlen(str) << endl; //==>5,字符串长度,不包含'/0'
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2.17 sizeof 求联合体的大小
#include <iostream>
using namespace std;
union u
{
double a;
int b;
};
union u2
{
char a[13];
int b;
};
union u3
{
char a[13];
char b;
};
int main()
{
cout << "sizeof(double)=" << sizeof(double) << endl; //==>8
cout << "sizeof(u)=" << sizeof(u) << endl; //==>8,union,分配内存的大小取决于,它的成员中占用空间最大的一个,
//同时还要满足**字节对齐**;
cout << "sizeof(u2)=" << sizeof(u2) << endl; //==>16,本应该分配13个字节,但是有int 类型,所以还应该满足,字节对齐;
cout << "sizeof(u3)=" << sizeof(u3) << endl; //==>13
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2.17 #pragma pack
#pragma pack 的主要作用就是改变编译器的内存对齐方式,这个指令在网络报文的处理中有着重要的作用,
#pragma pack(n)是他最基本的用法,其作用是改变编译器的对齐方式, 不使用这条指令的情况下,编译器
默认采取#pragma pack(8)也就是8字节的默认对齐方式,n值可以取(1, 2, 4, 8, 16) 中任意一值。
#include <iostream>
using namespace std;
/*
#pragma pack 的主要作用就是改变编译器的内存对齐方式,这个指令在网络报文的处理中有着重要的作用,
#pragma pack(n)是他最基本的用法,其作用是改变编译器的对齐方式, 不使用这条指令的情况下,编译器
默认采取#pragma pack(8)也就是8字节的默认对齐方式,n值可以取(1, 2, 4, 8, 16) 中任意一值。
*/
#pragma pack(16) //设置一个字节对齐;
struct test
{
char c;
short s1;
short s2;
int i;
};
int main()
{
test ts;
cout<<sizeof(ts)<<endl; //==>9
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2.18 内联函数
内联函数是C++中的一种特殊函数,它可以像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的,这样可以大大减少
由函数调用带来的开销,从而提高程序的运行效率。一般来说inline用于定义类的成员函数。
inline的使用比较简单,只需要在声明或者定义函数时在头部加上inline关键字即可,格式如下:
inline 返回值类型 函数名(函数参数){}
1
一般来说,inline适用的函数有两种,一种是在类内定义的成员函数,另一种是在类内声明,类外定义的成员函数,对于这两种情况inline的使用有一些不同:
(1)类内定义成员函数
这种情况下,我们可以不用在函数头部加inline关键字,因为编译器会自动将类内定义的函数声明为内联函数,代码如下:
class temp{
public:
int amount;
//构造函数
temp(int amount){
this->amount = amount;
}
//普通成员函数,在类内定义时前面可以不加inline
void print_amount(){
cout << this-> amount;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
从上面的代码可以看出,在类内定义函数时,可以不加inline关键字,编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)设置为内联,具有内联函数调用的性质。
(2) 类内声明函数,在类外定义函数
根据C++编译器的规则,这种情况下如果想将该函数设置为内联函数,则可以在类内声明时不加inline关键字,而在类外定义函数时加上inline关键字,代码如下所示:
class temp{
public:
int amount;
//构造函数
temp(int amount){
this->amount = amount;
}
//普通成员函数,在类内声明时前面可以不加inline
void print_amount()
}
//在类外定义函数体,必须在前面加上inline关键字
inline void temp:: print_amount(){
cout << amount << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
内联函数的优点:
1.inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
2.很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。(宏替换不会检查参数类型,安全隐患较大)
3.inline函数可以作为一个类的成员函数,与类的普通成员函数作用相同,可以访问一个类的私有成员和保护成员。内联函数可以用于替代一般的宏定义,最重要的应用在于类的存取函数的定义上面。
内联函数的缺点:
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率.如果执行函数体内代码的时间相比函数调用的开销较大时,效率就不如采用调用方式高;
内联函数具有一定的局限性,内联函数的函数体一般来说不能太大,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。(换句话说就是,你使用内联函数,只不过是向编译器提出一个申请,编译器可以拒绝你的申请)这样,内联函数就和普通函数执行效率一样了。
2.19 内联函数与宏的区别
内联函数在编译时展开,宏在预编译时展开;
在编译的时候,内敛函数可以直接被镶嵌到目标代码中,而宏只是一个简单文本替换;
内联函数在编译时,对语法进行检查,宏定义在使用时只是简单的文本替换,并没有做严格的参数检查,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
3.1 一般引用
进行引用时,新定义的引用变量不开辟存储空间;
一般引用必须在声明的同时进行绑定,后面不能重新绑定变量,即,引用只能在声明的时候被赋值,以后都不能再把引用名作为其他变量名的别名;
;
#include <iostream>
using namespace std;
int main()
{
int a = 50, b = 100;
int &c = a; //!不需要给c重新开辟新的存储空间,a与c占用一个存储单元;
cout << a << " " << b << endl; //==>50 100
c = 25;
cout << a << " " << b << endl; //==>25 100
int qual=(&a==&c)?1:0;
cout<<qual<<endl; //==> 1,由此可见,将c声明为a的别名,不需要给c重新开辟新的存储空间,a与c占用一个存储单元;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3.2 指针变量的引用;
指针变量引用必须在声明的同时进行绑定,后面可以重新绑定变量;
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int b = 10;
int *p = &a;
int *&pa = p; //指针变量的引用,需要在定义时进行绑定;
(*pa)++; //pa是p的别名;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "*p=" << *p << endl;
pa = &b; //pa绑定到b;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "*p=" << *p << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
3.3 变量引用
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int b = 2;
int &c; //!错误,引用类型变量在声明的同时**必须**初始化;
int &d = a;
&d = b;//!错误,将d当做b的别名,引用只能在声明的时候被赋值,以后都不能再把引用名作为其他变量名的别名;
int *p; //! 野指针;容易出错;
*p = 5;
cout << *p << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3.4 参数引用
#include <iostream>
using namespace std;
const float pi=3.1415926;
float f;
float f1(float r) //返回值,通过该函数返回引用,只能返回一个临时变量,所以会出现编译错误;
{
f=r*r*pi;
cout<<f<<endl;
return f;
}
float & f2(float r) //返回引用
{
f=r*r*pi;
return f;
}
int main()
{
//--------------------------------------------------------
float f1(float=5);//!声明f1()的默认参数调用,默认值为5;
float& f2(float=6);//!声明f2()的默认参数调用,默认值为6;
//--------------------------------------------------------
float a=f1();
float &b=f1(); //错误;将变量b赋为f1()的返回值.因为在f1()函数里,全局变量f的值78.5赋值给一个**临时变量**,
//这个临时变量是由编译器**隐式**地建立,然后建立这个临时变量的引用,此时对临时变量进行引用,就会发生编译错误;
float c=f2();
float &d=f2(); //此种方式返回的是全局变量的引用,而全局变量的声明周期大于d,所以引用是有效的;
cout<<f<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3.5 引用错误
const 类型可以引用非const 类型:
int a=10;
const int &b=a;//此时b是const 常量,所以,不能改变b的值,例如:b++;是违法的;
1
2
非const 类型,不能引用const:
const int a=10;
int &b=a;//b是一个变量,可以改变其值,但是a是一个常量;
1
2
#include <iostream>
using namespace std;
class Test
{
public:
void func(const int &arg)
{
//arg=10;//! 错误:arg是一个常量的引用,所以arg的值在函数内不能被修改;
cout << "arg=" << arg << endl;
value = 20;
}
private:
int value;
};
int main()
{
int a = 7;
const int b = 10;
//int &c=b; //!错误,const 类型可以引用变量,但是,变量不可以引用const类型;==>const int &c=b;
const int &d = a;
a++;
//d++; //! d是常量引用,不能对d赋值,但是,可以改变a的值,所以const 变量是不能直接改变其值,但是可以间接改变其引用的值;
Test test;
test.func(a); //!
cout << "a=" << a << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3.6 指针和引用的区别:
初始化要求不同:
引用: 引用在创建的同时必须初始化,即引用到一个有效的对象;
指针: 指针在创建的时候,可以不初始化,可以在后面重新赋值;
可修改性不同:
引用:引用一旦初始化为指向一个对象,它就不能被改变为另一个对象的引用;
指针:指针在任何时候都可以改变指向一个对象;
不存在NULL引用:
引用: 引用不能使用指向空值的引用,它必须总是指向某一个对象;
指针: 可以指向NULL;
3.7 传引用比传指针安全:
引用:
由于引用不存在空引用,并一旦初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此比较安全;
指针:
指针可以随时改变指向别的对象,并且可以不被初始化或为NULL,甚至可以出现野指针,所以不安全;
3.8 指针数组与数组指针
指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
int *p1[5];
int (*p2)[5];
1
2
首先,对于语句“intp1[5]”,因为“[]”的优先级要比“”要高,所以 p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,如图 1 所示,因此,它是一个指针数组。
其次,对于语句“int(p2)[5]”,“()”的优先级比“[]”高,“”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图 2 所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。
由此可见,对指针数组来说,首先它是一个数组,数组的元素都是指针,也就是说该数组存储的是指针,数组占多少个字节由数组本身决定;而对数组指针来说,首先它是一个指针,它指向一个数组,也就是说它是指向数组的指针,在 32 位系统下永远占 4 字节,至于它指向的数组占多少字节,这个不能够确定,要看具体情况。
此段摘录:数组指针和指针数组的区别
#include <iostream>
using namespace std;
int main()
{
//指针数组:数组保存指针;
int *p1, *p2, *p3;
int *a[4] = {p1, p2, p3};
//arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。
char *arr[4] = {"hello", "world", "shannxi", "xian"};
for (int i = 0; i < 4; i++)
{
cout << arr[i] << endl;
}
//数组指针:一个指针指向一个数组;
int b[3] = {10, 20, 30};
int(*p)[3] = &b; //!一个指针,指向一个含有3个整型的数组;==>必须类型 数量 都对应相等;
/*下面是错误的*/
//int (*p2)[5] = b;
for (int i = 0; i < 3; i++)
{
cout<<p<<endl;
cout<<*p<<endl;
cout << "数组地址arr:"<<&b[i]<< endl;
cout << "数组地址*p:"<<(*p+i) << endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
指针数组与数组指针:TODO==>有点绕;
#include <iostream>
using namespace std;
int main()
{
//指针数组:数组里面存放的指针;
char *str[] = {"welcome", "to", "Fortemedia", "Nanjing"};
char **p = str + 1;
str[0] = (*p++) + 2;
str[1] = *(p + 1);
str[2] = p[1] + 3;
str[3] = p[0] + (str[2] - str[1]);
cout << str[0] << endl;
cout << str[1] << endl;
cout << str[2] << endl;
cout << str[3] << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3.9 指针加1
指针 + 1 并不是指针代表的地址值 + 1.一个类型为T的指针的移动,是以sizeof(T)为移动单位。
指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。至于真实的地址加了多少,要看原来指针指向的数据类型是什么。
char a = 'a';
char *p = &a;
cout<<(void*)p<<" "<<(void*)(p+1)<<endl;
//输出:0012FF33 0012FF34
1
2
3
4
p指向的是一个字符,p+1就是移动一个字符大小,一个字符就是一个字节,所以p +1 代表的地址就比 p 代表的地址大1。
int i = 1;
int *p = &i;
cout<<(void*)p<<" "<<(void*)(p+1)<<endl;
//输出:0012FF30 0012FF34
1
2
3
4
int i = 1;
int *p = &i;
cout<<(void*)p<<" "<<(void*)(p+1)<<endl;
//输出:0012FF30 0012FF34
1
2
3
4
参考链接
**&数组名+1:**移动一个数组大小,与&(数组名+1)不同,&(数组名+1)移动到数组下一个元素;
#include <iostream>
using namespace std;
int main()
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);//&a+1==> &a+sizeof(a),也就是a[5]的地址,显然已经超出数组的界限
cout<<*(a+1)<<endl;//a+1 ==>&a[0]+1==>a[1];
cout<<*(ptr-1)<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
(ptr-1)输出为多少? &a+1不是首地址+1,系统会认为加了一个a数组,偏移了整个数组a的大小(也就是5个int的大小)。所以intp=(int*)(&a+1);其实ptr实际是&(a[5]),也就是a+5.原因为何呢? &a是数组指针,其类型为int()[5];(指向含有5个int的数组), 而指针加1要根据指针类型加上一定的值,不同类型的指针+1之后增加的大小不同,a是长度为5的int数组指针,所以要加5sizeof(int),所以p实际是a[5],但是p与(&a+1)类型是不一样的,这点非常重要,所以ptr-1只会减去sizeof(int*),a,&a的地址是一样的,但意思就不一样了,a是数组首地址,也就是a[0]的地址,&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1],&a+1是下一个对象的地址,即a[5]。
a是数组首元素的地址;
&a是整个数组的首地址。二者值一样,但是意义却不相同。
数组名代表整个数组的时候只有两种情况:sizeof(数组名),这里的数组名表示整个数组。&数组名,这里的数组名表示整个数组。(对上例数组a,sizeof(a)的值为20,表示整个数组大小。sizeof(a+0)的值为4,因为类似a+0,a+1等数组名进行了偏移运算,那么它就代表指向某个元素的指针)
————————————————
版权声明:本文为CSDN博主「在下能猫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_23996069/article/details/89309016
3.10 相同内容的数据存储地址关系
#include <iostream>
using namespace std;
int main()
{
char str[] = "abc";
char str1[] = "abc";
const char str2[] = "abc";
const char str3[] = "abc";
const char *str4 = "abc";
const char *str5 = "abc";
char *str6 = "abc";
char *str7 = "abc";
/*
*数组 str str1 str2 str3 都是在栈中分配,内存中的内容都是"abc"+'/0';
*但是,他们的位置是不同的;
*/
cout << (str == str1) << endl; //==0
cout << (str2 == str3) << endl; //==0
/*
*数组 str4 str5 str6 str7 也是在栈中分配,他们都指向字符串"abc",由于"abc"存放在数据区,
*所以,str4 str5 str6 str7 其实指向同一块数据区内存;
*/
cout << (str4 == str5) << endl; //==1
cout << (str5 == str6) << endl; //==1
cout << (str6 == str7) << endl; //==1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3.11 内存越界
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
int main()
{
char a;
char *str1 = &a;
//strcpy(str1, "hello"); //!运行出错; str1 指向一个字节大小的内存,由于复制"hello"需要至少6个字节,显然内存不够;
//cout << str1 << endl;
//修改为:
/*
char a[10];
char *str1 = a;
strcpy(str1, "hello");
*/
char *str2="AAA";
// str2[0]='B'; //! str2 指向一个常量,因为是常量,所以不能进行重新赋值;
//修改为:
/*
char str2[]="AAA";
str2[0]='B';
*/
cout<<str2<<endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3.12 指针常量与常量指针
常量指针:就是指向常量的值指针,它指向的内容是不可以修改的;目的是为了防止程序过程中对指针误操作出现修改常量的错误;
指针常量:就是指针是常量,它是不可改变地址的指针,但是,可以对指针指向的内容进行修改;
3.13 this指针
下列关于this指针描述正确的是(A,B)
A. 类的非静态成员函数才有this指针;
B.类的友元函数,是非成员函数,所以不具有this指针;
3.14 函数指针与指针函数
指针函数:首先它是一个函数,返回类型是一个指针;
函数指针:首先它是一个指针,指向一个函数;
Tips: 像这样连着的两个词,前面的一个通常是修饰部分,中心部分在后面;
指针函数
指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。
声明格式为:
类型标识符 * 函数名(参数表)
int fun(int x,int y);
1
这种函数应该都很熟悉,其实就是一个函数,然后返回值是一个 int 类型,是一个数值。
接着看下面这个函数声明:
int *fun(int x,int y);
1
这和上面那个函数唯一的区别就是在函数名前面多了一个*号,而这个函数就是一个指针函数。其返回值是一个 int 类型的指针,是一个地址,一般返回一个地址是危险的,若返回一个局部变量的地址,该局部变量在其生命周期结束后,就会被销毁,此时该地址的内容是不确定
函数指针
与指针函数不同,函数指针 的本质是一个指针,该指针的地址指向了一个函数,所以它是指向函数的指针。所以调用时,需要指针函数的入口地址给指针;
我们知道,函数的定义是存在于代码段,因此,每个函数在代码段中,也有着自己的入口地址,函数指针就是指向代码段中函数入口地址的指针。
#include<iostream>
using namespace std;
/*
* 求最大值
* 返回值是int类型,返回两个整数中较大的一个
*/
int max(int a, int b) {
return a > b ? a : b;
}
/*
* 求最小值
* 返回值是int类型,返回两个整数中较小的一个
*/
int min(int a, int b) {
return a < b ? a : b;
}
int (*f)(int, int); // 声明函数指针,指向返回值类型为int,有两个参数类型都是int的函数
//函数指针,只需要声明,不需要实现;
int main(int argc, char* argv[])
{
f = max; // 函数指针f指向求最大值的函数max
int c = (*f)(1, 2);
printf("The max value is %d \n", c);
f = min; // 函数指针f指向求最小值的函数min
c = (*f)(1, 2);
printf("The min value is %d \n", c);
printf("------------------------------ End\n");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
多个函数指针使用
#include<iostream>
using namespace std;
int add1(int x,int y)
{
return x+y;
}
int add2(int x,int y)
{
return x+y;
}
int main(int argc, char* argv[])
{
int (*p[2])(int,int); //!此处定义了一个指针数组,数组存放两个指针,指向不同的函数;
p[1]=add1;
p[2]=add2;
cout<<"p[1](1,2)="<<p[1](1,2)<<endl;
cout<<"p[2](3,4)="<<p[2](3,4)<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
3.15 typedef 用于函数指针
#include<iostream>
using namespace std;
int add1(int x,int y)
{
return x+y;
}
typedef int (*pFun)(int, int);//! typedef 用于函数指针
//! 这里的pFun是一个使用typedef 定义的数据类型,表示一个函数指针
//!其参数有两个,都是int型,返回值也是int型;
int (*pFun2)(int,int);// 与上式作对比
int main(int argc, char* argv[])
{
//------------------------------------------------
pFun fun=add1; //! 用 pFun类型定义一个fun,然后调用函数
cout<<fun(2,3)<<endl;
//------------------------------------------------
pFun2=add1;
cout<<(*pFun2)(3,4)<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3.16 野指针
野指针,不是指向NULL的指针,而是指向"垃圾"内存的指针.其原因主要是:指针变量没有被初始化,或指针p被free或者delete之后,没有置为NULL;
为了防止出现野指针,所以应该在初始化的将指针置空;
3.17 有了malloc/free 为什么还需要new/delete
new/delete和malloc/free的区别
malloc和free是库函数,而new和delete是C++操作符;
new自己计算需要的空间大小,比如’int * a = new,malloc需要指定大小,例如’int * a = malloc(sizeof(int))’;
opeartor new /operator delete可以重载,而malloc不行
malloc能够直观地重新分配内存
使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。
对于自定义对象,对象的消亡之前要自动执行析构函数,由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3.17 内存越界
#include <iostream>
using namespace std;
extern "C"
{
#include <string.h>
}
int main(int argc, char *argv[])
{
char *strp;
char *str = "test";
/*内存越界*/
// strp=new char[strlen(str)];
// strcpy(strp,str);//!内存越界;由于str的长度还有一个'/0';
strp = new char[strlen(str) + 1]; //新建内存容量,应该+1
strcpy(strp, str);
cout << str << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3.18 动态内存传递
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
char *getMemory_(int num)
{
char *p = new char[sizeof(char) * num];
return p;
}
void getMemory(char *p, int num)
{
p = new char[sizeof(char) * num];
cout << sizeof(p) << endl;
}
int main(int argc, char *argv[])
{
char *strp = nullptr;
int num = 10;
//-------------------------------
getMemory(strp, num);
//cout<<strlen(strp)<<endl;;//!出错,因为调用getMemory函数体内的p实际上是main函数中strp的备份,变量在getMemory()
//!函数栈中的一个备份,因为编辑器总是为函数的每一个参数制作临时变量,因此在getMemory()申请
//!堆内存,但是返回main函数时,str还是NULL,并不指向那块内存,所以调用此句会出现内存错误;
//-改正为:在子函数中生成一段内存,并将指向这段内存的指针返回;
char *strp_=getMemory_(1);
cout<<strlen(strp_)<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
对于内存泄露可以通过一下三种方法进行传递动态内存:
采用指向指针的指针,可以将str的地址传给函数GetMemory();
传递指针的引用;
使用返回值来传递动态内存;
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
char *getMemory1(int num)
{
char *p = new char[sizeof(char) * num];
return p;
}
void getMemory2(char *&p, int num) //! 传递指针的引用;
{
p = new char[sizeof(char) * num];
}
void getMemory3(char **p, int num)
{
*p = new char[sizeof(char) * num];
}
int main(int argc, char *argv[])
{
char *strp1 = getMemory1(20);
char *strp2 = nullptr, *strp3 = nullptr;
getMemory2(strp2, 20);
getMemory3(&strp3, 20);
strcpy(strp1, "getMemory 1");
strcpy(strp2, "getMemory 2");
strcpy(strp3, "getMemory 3");
cout << "strp1=" << strp1 << endl;
cout << "strp2=" << strp2 << endl;
cout << "strp3=" << strp3 << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
4.0 易错
#include <iostream>
#include <string>
using namespace std;
int main(int agrv, char *agrc[])
{
char *str="hello world!";
cout<<str<<endl; //==>hello world!,输出的不是地址 !!!
cout<<*str<<endl; //!==>h 解索引输出的是首地址对应的内存内容;
string str1="test string";
cout<<str1<<endl;
string *str2=&str1;
cout<<*str2<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
4.0.1 string 与char *的区别
定义:
string:string是STL当中的一个容器,对其进行了封装,所以操作起来非常方便。
char*:char *是一个指针,可以指向一个字符串数组,至于这个数组可以在栈上分配,也可以在堆上分配,堆得话就要你手动释放了。
区别:
string的内存管理是由系统处理,除非系统内存池用完,不然不会出现这种内存问题。
char *的内存管理由用户自己处理,很容易出现内存不足的问题。
4.1 不使用库函数将数字转换成char
要想将数字转换成字符,将每一个位置上的数字+'0',就可以将此位置上的数字从int转换为char;
注意:数字只能取正数,不能取负数;
0+‘0’=‘0’;
1+‘0’=‘1’;
…
9+‘0’=‘9’;
所以解法:
首先判断要转换的数字的正负性;负数取绝对值;
提取每一位,然后加'0',转换成char;
负数前面加'-';
字符串的最后需要加上'\0'
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
void int2char(int n, char *str)
{
char buff[10] = "";
int i = 0;
int len = 0;
int temp = (n > 0) ? n : -n;
if (str == nullptr)
{
return;
}
while (temp)
{
buff[i++] = temp % 10 + '0'; //提取每一位数字;
temp = temp / 10; //下一位;
//例如:temp=123
//1. temp%10=12...3,temp%10=3;temp/10=12;temp=12;
//2. temp%10=1...2;temp%10=2;temp/10=2;temp=1;
//1.temp%10=0...1;temp%10=1;temp/10=0;temp=0;退出
//此时buff中保存的是倒叙:321;
}
cout<<i<<endl;
len = n < 0 ? ++i : i; //如果n是负数,则需要添加一位来保存'-'号;
str[i] = '\0'; //输出字符串,末尾结束符;'\0'
cout<<len<<endl;
while (1)
{
i--;
cout<<i<<endl;
if (buff[len - i - 1] == '\0')
{
break;
}
str[i] = buff[len - i - 1]; //倒叙转正序;
}
if (i == 0)
{
str[i] = '-';
}
}
int main(int argc, char *argv[])
{
char p[10];
int2char(-123, p);
cout << p << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
4.2 字符转数字
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
int str2int(const char *str)
{
int temp=0;
const char *ptr=str;
if(*str=='-'||*str=='+') //如果第一字符是正负号,则移动到下一个字符;
{
str++;
}
while (*str!='0')//==>while (*str!=0) //!'\0'代表ASCII为0的字符,从ASCII表上可以知道,ASCII为0,是一个空字符,不可见.
{
if(*str<'0'||*str>'9') //如果字符不是数字,则退出
{
break;
}
temp=temp*10+(*str-'0');//将字符转换成数字==>-'0';
str++;
cout<<temp<<endl;
}
return temp;
}
int main(int argc, char *argv[])
{
int num=str2int("123");
cout<<num<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
4.3 strcpy()的实现
#include <iostream>
using namespace std;
void strCopy(char *strDest, const char *str)
{
if ((strDest == NULL) || (str == NULL))
{
return;
}
const char *desstr = strDest; //保存目标首地址;
while ((*strDest++ = *str++) != '\0')
{
}
return;
}
int main(int agrv, char *agrc[])
{
char *str1 = "hello world";
char str2[20];
strCopy(str2, str1);
cout << str2 << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
4.4 寻找子串
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
char *comonString(char *str1, char *str2)
{
int i, j;
char *longstr, *shorstr;
if ((str1 == NULL) || (str2 == NULL))
{
return NULL;
}
if (strlen(str1) <= strlen(str2))
{
shorstr = str1;
longstr = str2;
}
else
{
shorstr = str2;
longstr = str1;
}
if (strstr(longstr, shorstr) != NULL) //!采用strstr()在一个字符串中,寻找子串;
//! 如果在长的子串中能寻到短的子串,返回短子串;
{
return shorstr;
}
char *substr = new char[strlen(shorstr) + 1]; //用于保存子串
for (i = strlen(shorstr) - 1;i > 0; i--)
{
cout<<"strlen(shorstr) - 1="<<strlen(shorstr) - 1<<endl;
for (j = 0; j < strlen(shorstr) - i; j++)
{
cout<<"strlen(shorstr) - i="<<strlen(shorstr) - i<<endl;
memcpy(substr, &shorstr[j], i); //将短字符串的一部分复制到substr,其长度逐渐减小;
substr[i] = '\0';
if (strstr(longstr, substr) != NULL) //在longstr中寻找子串;
{
return substr;
}
}
}
return NULL;
}
int main(int agrv, char *agrc[])
{
char *str1="find test";
char *str2="test_";
//cout<<strlen(str2)<<endl; //字符串长度:4;
char *common=comonString(str1,str2);
cout<<common<<endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
4.5 常用c字符串操作
此部分内容来自:版权声明:本文为CSDN博主「芮小谭」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tanrui519521/article/details/81162267
strlen()
size_t strlen( const char* str)
1
功能:计算字符串长度,不包含’\0’
返回值:返回字符串的字符数;
strlen() 函数计算的是字符串的实际长度,遇到第一个’\0’结束;
参数指向的字符串必须以 ’ \0 ‘结束
函数返回值一定是size_t ,是无符号的
如果你只定义没有给它赋初值,这个结果是不定的,它会从首地址一直找下去,直到遇到’\0’停止
sizeof返回的是变量声明后所占的内存数,对于字符串会包含’\0’所占用的内存,不是实际长度,此外sizeof不是函数,仅仅是一个操作符,strlen()是函数;
#include <iostream>
extern "C"
{
#include <string.h>
}
using namespace std;
int main(int agrv, char *agrc[])
{
char str1[] = "test";
cout << sizeof(str1) << endl; //==>5 因为'\0'占有内存;
cout << strlen(str1) << endl; //==>4 不包含'\0'长度;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
strcpy()
char* strcpy(char* dest,char* src)
1
功 能: 将参数src字符串拷贝至参数dest所指的地址
返回值: 返回参数dest的字符串起始地址
源字符串必须以’\0’结束
会将源字符串的’\0’拷贝到目标空间
目标空间必须可变
如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况,在编写程序时需特别留意,或者用strncpy()来取代
strncpy()
char* strncpy(char* dest,const char* src,size_t num)
1
功能:拷贝src字符串的前num个字符至dest
返回值:dest字符串起始地址
说明:
如果src字符串长度小于num,则拷贝完字符串后,在目标后追加0
strncpy不会向dest追加’\0’
src和dest所指的内存区域不能重叠,且dest必须有足够的空间放置n个字符
strcat()
char* strcat(char* dest,const char* src)
1
功能: 字符串拼接
返回值:返回dest字符串起始地址
说明:
源字符串必须’\0’结束
目标空间必须可修改
strcat() 会将参数src字符串复制到参数dest所指的字符串尾部
dest最后的结束字符’\0’会被覆盖掉,并在连接后的字符串的尾部再增加一个’\0’
dest与src所指的内存空间不能重叠,且dest要有足够的空间来容纳要复制的字符串
strncat()
char* strncat (char* dest,const char* src,size_t num)
1
功能:将n个字符追加到字符串结尾
返回值:返回dest字符串的起始地址
说明:
strncat将会从字符串src的开头拷贝n个字符到dest字符串尾部
dest要有足够的空间来容纳要拷贝的字符串
如果n大于字符串src的长度,那么仅将src全部追加到dest的尾部
strncat会将dest字符串最后的’\0’覆盖掉,字符追加完成后,再追加’\0’
6.strcmp()
int strcmp (const char* str1,const char* str2)
1
功能:字符串比较
返回值:若参数s1和s2字符串相同则返回0,s1若大于s2则返回大于0的值,s1若小于s2则返回小于0的值
说明:
判断两个字符串大小1)ASII码 2)长度
区分大小写比较的,如果希望不区分大小写进行字符串比较,可以使用stricmp函数
strncmp()
int strncmp(const char* str1,const char* str2,size_t num)
1
功能:指定长度比较
8.strstr()
char* strstr(const char* str,const char* substr)
1
功能:检索子串在字符串中首次出现的位置
返回值:返回字符串str中第一次出现子串````的地址;如果没有检索到子串,则返回NULL
5.位制转换
在32位计算机中,int与float 均占4个字节,double占有8个字节;一般情况下,在c++中,使用cout输出是没有问题的,但是采用printf()会涉及位制转换导致读取错误;
printf()会根据说明符"%f".编译器认为参数应该是double类型(在printf函数中,float会自动转换成为double),因此会从栈中读出8个字节.类似地,当printf()说明符为"%d"时,编译器认为参数应该是int类型,因此会从栈中读出4个字节;
#include<iostream>
using namespace std;
int main()
{
printf("%f\n",5);//可能存在内存越界的错误:参数5是一个int型,所以在栈中分配了4个字节的内存用于存放参数5,然后在printf从栈中读取8个字节;
printf("%d\n",5.01);//出错!参数5.01占用8个字节,读取时读取了4个字节,所以内存越界;
printf("%f\n",5.01);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
6.1 private 数据类型
private:用来指定私有成员。一个类的私有成员,不论是成员变量还是成员函数,都只能在该类的成员函数内部才能被访问。
#include <iostream>
using namespace std;
class A
{
private:
void print()
{
cout << "private print" << endl;
}
public:
void print2()
{
print();//一个类的私有成员,不论是成员变量还是成员函数,都只能在该类的成员函数内部才能被访问。
cout<< "public print" << endl;
}
};
int main()
{
A a;
a.print2();
//a.print(); 无法调用
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
6.2 初始化的坑
#include <iostream>
using namespace std;
class A
{
private:
int i;
int j;
public:
A(int x):j(x),i(j){}
void print()
{
cout<<"i="<<i<<"\n"<<"j="<<j<<endl;
}
};
int main()
{
A a(10);
a.print();//==>i=0;j=10;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
此处初始化完成后,i!=10;(错误认为:先用10对j初始化,然后再用j对i初始化,所以两者的结果都是10 )
初始化成员列表的初始化顺序与变量声明的顺序一致:而不是按照出现在初始化列表中顺序;
这里成员i比成员j先声明,因此成员i先被初始化,而此时j未被初始化,j是一个随机值,故i的值也是随机值;(==0?)
6.3 析构函数与构造函数是否可以被重载
构造函数:可以重载;
析构函数:不可以,析构函数只能有一个;
6.4 构造函数中explict与普通函数的区别
#include <iostream>
using namespace std;
class Test1
{
private:
int num;
public:
Test1(int n):num(n){}
};
class Test2
{
private:
int num;
public:
explicit Test2(int n):num(n){} //explicit(明确的) 显示构造函数
};
int main()
{
Test1 test1(10);
Test1 test2=20; //隐式调用其构造函数
Test2 test3(30);
//Test2 test4=40; //编译错误,不能通过隐式转换调用其构造函数;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
6.5 explicit 的作用
#include <iostream>
#include <string>
using namespace std;
class Number
{
public:
string type;
Number() : type("void") {}
explicit Number(short) : type("short") {}
Number(int) : type("int") {}
};
void show(const Number &n) { cout << n.type << endl; }
int main()
{
short s = 42; //'='表示赋值,即隐式转换
// Number num=s; //采用隐式调用;
// show(num); //!输出==>int
show(s); //!输出==>int
//!原因如下:show(s)的s为short类型,其值为42,因此会首先检查参数为short的构造函数是否被隐式转换.
//!由于short类型的构造函数被声明为explict 所以不可以被隐式转换;
//!42 会被自动转换成int 类型;
//!检查int类型是否可以被隐式转换,int类型的构造函数没有explicit 声明,所以可以进行隐式转换,因此输出int
short ss = 45;
Number num2(ss);
show(num2); //输出==>short
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
6.6 继承类析构方式
#include <iostream>
#include <string>
using namespace std;
class A
{
private:
int a;
public:
A(int aa);
~A();
};
A::A(int aa):a(aa)
{
}
A::~A()
{
cout<<"destructor A"<<a<<endl;
}
class B:public A
{
private:
int b;
public:
B(int aa,int bb);
~B();
};
B::B(int aa=0,int bb=0):A(aa),b(bb)
{
}
B::~B()
{
cout<<"destructor B"<<b<<endl;
}
int main()
{
B obj(5),obj2(6,7);//==>输出
/* destructor B7
destructor A6
destructor B0
destructor A5
*/
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
所以析构函数执行不光只执行继承类的析构函数,还要执行基类的析构函数!!
7.类的继承
类的成员数据类型;
protected:protectd成员数据或者成员函数,只能被本类或本类派生类中的成员函数访问;
private:private成员数据或者成员函数,只能被本类中的成员函数访问;
public :public成员数据或者成员函数,既可以被本类的成员函数访问,也可以被类外的函数访问
类继承方式:
public 继承:基类的中的**公有成员***(成员:成员函数与成员数据)和保护成员***,在派生类中的访问权限不变,仍然是公有成员和保护成员;私有成员,在继承类中访问权限是不可访问;
protected 继承:基类的中的公有成员(成员:成员函数与成员数据)和保护成员,在派生类中的访问权限变为保护成员;基类中公有成员和保护成员在派生类内部都是可以访问的;私有成员,在继承类中访问权限是不可访问;
private 继承: 基类的中的公有成员(成员:成员函数与成员数据)和保护成员,在派生类中的访问权限变为私有成员;私有成员,在继承类中访问权限是不可访问;
由此可见,无论何种继承方式,基类的private成员,均是不可以访问;
基类中的成员 在公有派生中的访问属性 在保护派生中的访问属性 在私有派生中的访问属性
私有成员 不可访问 不可访问 不可访问
保护成员 保护 保护 私有
公有成员 公有 保护 私有
#include <iostream>
#include <string>
using namespace std;
class A
{
private:
int a;
void printf_private()
{
cout << a << b << c << endl;
}
protected:
int b;
void printf_protected()
{
cout << a << b << c << endl;
}
public:
int c;
A(int a_, int b_, int c_) : a(a_), b(b_), c(c_){};
void printf_public()
{
cout << a << b << c << endl;
}
};
class B : public A
{
public:
B(int a_, int b_, int c_) : A(a_, b_, c_) {}
void B_print_public()
{
printf_protected();
printf_public();
//cout<<a<<endl; //public 继承后,基类的私有成员或成员函数均是不能访问的;
cout << b << endl;
cout << c << endl;
}
};
class C : protected A
{
public:
C(int a_, int b_, int c_) : A(a_, b_, c_) {}
void C_print_public()
{
printf_protected();
printf_public();
//cout<<a<<endl;//protected 继承后,基类的私有成员或成员函数均是不能访问的;
cout << b << endl;
cout << c << endl;
}
};
class D : private A
{
public:
D(int a_, int b_, int c_) : A(a_, b_, c_) {}
void D_print_public()
{
printf_protected();
printf_public();
//cout<<a<<endl; //private 继承后,基类的私有成员或成员函数均是不能访问的;
cout << b << endl;
cout << c << endl;
}
};
int main()
{
B b_public(1, 2, 3);
C c_protected(1, 2, 3);
D d_private(1, 2, 3);
b_public.printf_public();
b_public.B_print_public();
cout << b_public.c << endl; //public 继承的public成员数据,可以被访问
c_protected.C_print_public();
//c_protected.printf_public(); //保护继承,只能在类的成员函数中访问
//cout<< c_protected.c<<endl; //保护继承,基类的public成员,访问属性为protected,只能在类的成员函数内部访问;
d_private.D_print_public();
//d_private.printf_public(); //私有继承,只能在类的成员函数中访问;
//cout<<d_private.c<<endl; //私有继承,基类的public成员,访问属性为private,只能在类的成员函数内部访问;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
7.1 继承中构造函数调用方式
#include <iostream>
#include <string>
using namespace std;
void prinln(const std::string &msg)
{
cout << msg << endl;
}
class Base
{
public:
Base()
{
prinln("Base::Base()");
virt();
}
void f()
{
prinln("Base::f()");
virt();
}
virtual void virt()
{
prinln("Base::virt()");
}
};
class Derived : public Base
{
public:
Derived()
{
prinln("Derived::Derived()");
virt();
}
virtual void virt()
{
prinln("Derived::virt()");
}
};
int main()
{
Derived d;
Base *pB = &d;
pB->f();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
构造Derived 对象d.首先调用Base的构造函数,然后调用Derived的构造函数.在Base类的构造函数中,有调用了虚函数virt(),此时虚拟机制还没有开始作用(因为是在构造函数中),所以执行的是Base类的vitr()函数.同样,在Derived类的构造函数中,执行的是Derived类的virt函数;
通过Base类的指针pB访问Base类的公有成员函数f(),f()函数又调用了虚函数virt(),这里出现了多态,由于指针pB是指向Derived类对象的,因此实际执行的Derived类的virt()成员;
所以,对于继承类说,如果是构造函数, 则首先构造基类的构造函数,然后执行继承类的构造函数;对于析构函数,则是先析构继承类,然后析构基类;,
7.2 虚函数与纯虚函数的区别
类里面如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是能让这个函数在它的子类中里面可以覆盖,这样编译器就可以使用后期绑定来达到多态了.虚函数只是一个接口,是个函数的声明而已,他要留到子类里去实现;
虚函数在子类里面可以不重载,但纯虚函数必须要到子类去实现;
所以虚函数的目的是为了实现多态,纯虚函数的作用是为了预留一个接口和多态的实现;
带纯虚函数的类叫做虚基类,这种基类不能直接定义对象,而只有被继承,并重写了其虚函数后,才能使用,但可以定义其指针或引用 ;
7.2.1 虚函数
#include<iostream>
#include<cmath>
using namespace std;
const double PI=3.1415926;
class Circle
{
protected:
double r;
public:
Circle(double rad)
{
r=rad;
}
double Peri()
{
return 2*PI*r;
}
virtual double Area() //基类虚函数
{
return PI*r*r;
}
};
class Cyclinder:public Circle
{
double h;
public:
Cyclinder(double rad,double heigth):Circle(rad)
{
h=heigth;
}
double Area() //!==>尽管重新定义了Area(),但新定义的Area继承了基类的虚特性,成为虚函数,具有虚函数的特性; ==> virtual double Area()
{
return Peri()*h;
}
};
class Cone:public Circle
{
double h;
public:
Cone(double rad,double heigth):Circle(rad)
{
h=heigth;
}
double Area() //!==>尽管重新定义了Area(),但新定义的Area继承了基类的虚特性,成为虚函数,具有虚函数的特性; ==> virtual double Area()
{
return PI*r*sqrt(r*r+h*h);
}
};
void fun(Circle *pb)
{
cout<<pb->Area()<<endl;
}
int main()
{
Cyclinder cy(3,5);
Cone cn(3,5);
cout<<"圆柱体的侧面积是:";
fun(&cy);
cout<<"圆锥体的侧面积是:";
fun(&cn);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
由于在基类Circle中将Area函数定义为虚函数(A行),在其派生类Cylinder和Cone中,尽管也重新定义了Area(B行和C行),但新定义的Area继承了基类的虚特性,成为虚函数,具有虚函数的特性.这样,当基类指针指向派生类对象,并且用基类指针调用虚函数(pb->Area())时,执行的是派生类中新定义的同名函数,即分别求Cylinder和Cone的侧面积;
7.2.2 纯虚函数
#include<iostream>
#include<cmath>
using namespace std;
const double PI=3.1415926;
class Circle
{
protected:
double r;
public:
Circle(double rad)
{
r=rad;
}
double Peri()
{
return 2*PI*r;
}
virtual double Area()
{
return PI*r*r;
}
virtual double volume()=0; //纯虚函数,预留接口,保证多态性
};
class Cyclinder:public Circle
{
double h;
public:
Cyclinder(double rad,double heigth):Circle(rad)
{
h=heigth;
}
double Area()
{
return Peri()*h;
}
virtual double volume()
{
return Circle::Area()*h; //冲突与支配,否则会执行派生类中的Area()
}
};
class Cone:public Circle
{
double h;
public:
Cone(double rad,double heigth):Circle(rad)
{
h=heigth;
}
double Area()
{
return PI*r*sqrt(r*r+h*h);
}
virtual double volume()
{
return Circle::Area()*h/3; //冲突与支配,否则会执行派生类中的Area()
}
};
void fun(Circle *pb)
{
cout<<pb->Area()<< " "<<pb->volume()<<endl;
}
int main()
{
//Circle circle(5); //!抽象类不能定义对象;
Circle *circle_ptr; //!但可以定义指针与引用;
Cyclinder cy(3,5);
Cone cn(3,5);
cout<<"圆柱体的侧面积和体积是:";
fun(&cy);
cout<<"圆锥体的侧面积和体积是:";
fun(&cn);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
8.1 STL
#include <iostream>
#include <vector>
#include <thread>
using namespace std;
int main()
{
vector<int> array;
for (int i =1; i < 4; i++)
{
array.push_back(i);
}
for(vector<int>::size_type i=array.size()-1;i>=0;i--)
{
cout<<array[i]<<endl;
this_thread::sleep_for(chrono::seconds(1));
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
此程序是一个死循环!!原因在与vector<int>::size_type的类型;
vector<int>::size_type<==typedef _SIZT size_type;<==typedef unsigned int size_t;所以size_type 是一个unsigned int 类型,是一个**>=0**的数,所以此循环会导致死循环:
#include <iostream>
#include <vector>
#include <thread>
using namespace std;
int main()
{
vector<int> array;
for (int i = 1; i < 4; i++)
{
array.push_back(i);
}
// for(vector<int>::size_type i=array.size()-1;i>=0;i--)
// {
// cout<<array[i]<<endl;
// this_thread::sleep_for(chrono::seconds(1));
// }
for (vector<int>::size_type j = array.size(); j > 0; j--)
{
cout << array[j - 1] << endl;
this_thread::sleep_for(chrono::seconds(1));
}
for (int k = array.size()-1; k >= 0; k--)
{
cout << array[k] << endl;
this_thread::sleep_for(chrono::seconds(1));
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
8.2 STL 删除某一个元素后,会自动移项
/*
*删除程序中所有的2;
*/
#include <iostream>
#include <vector>
#include <thread>
using namespace std;
int main()
{
vector<int> array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(3);
for (vector<int>::iterator itor = array.begin(); itor != array.end(); itor++)
{
if (2 == *itor)
{
array.erase(itor); //通过此方法只会删除一个2;
//! 这是因为每次调用"array.erase(itor);",被删除的元素之后的内容就会自动往前移动,
//!导致漏项,应该在删除一项后使itor--,使之从已经前移的下一个元素起继续遍历;
itor--; //添加此句,才不会导致漏项!!
}
}
for (auto &index : array)
{
cout << index << endl;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
array.erase(itor); //通过此方法只会删除一个2;
//! 这是因为每次调用"array.erase(itor);",被删除的元素之后的内容就会自动往前移动,
//!导致漏项,应该在删除一项后使itor–,使之从已经前移的下一个元素起继续遍历;
8.3 list 与vector的区别
vector 与数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此他能非常好的支持随机存取(使用[]操作符访问其中的元素).但是,由于它的内存空间是连续的,所以在进行插入操作和删除操作会造成内存的块拷贝,当内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝;
list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的,所以插入与删除效率很高;
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Destiny_zc/article/details/118532083