目录
一. C++基本语法与特性
1.C语言与C++语言的区别
- 结构不同
C语言:C语言结构只有成员变量,而没成员方法。
C++:C++结构中可以有自己的成员变量和成员函数。 - 设计不同
C语言:C语言进行过程化、抽象化的通用程序设计。
C++:C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。 - 函数库不同
C语言:C语言有标准的函数库,它们松散的,只是把功能相同的函数放在一个头文件中。
C++:C++对于大多数的函数都是有集成的很紧密,是一个集体。
可以理解为C++是C的一个超集,在C的基础上增加了一些方便的函数,但是也有一些细节不同
2.指针与引用的特性与区别
-
引用概念上定义一个变量的别名,指针存储一个变量地址
-
引用在定义时必须初始化,指针没有要求
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型 实体
-
没有NULL引用,但有NULL指针
-
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
-
指针有多级指针,但是引用没有多级引用
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
-
引用比指针使用起来相对更安全
3.野指针的概念以及如何避免
概念:“野指针”则是不确定其具体指向的指针,有的地方称指向被释放内存的指针为“悬空指针”,也就是释放空间后,指针没有置空;但其实这俩没事太大区别。
1.未初始化
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认就是随机值
*p=10;
return 0;
}
2.数组越界
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认就是随机值
*p=10;
return 0;
}
2.数组越界
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认就是随机值
*p=10;
return 0;
}
3.地址空间已经释放(包括free释放空间的情况)
//生存周期结束,内存空间释放
int* test()
{
int a=10;
return &a;
}
int main()
{
int* p=test();
printf("%d\n",*p);
return 0;
}
//free释放内存空间
int main()
{
int* p=(int *)malloc(sizeof(int));
free(p);
printf("%d\n",*p);
return 0;
}
如何规避野指针:
- 指针初始化 (如果没有就初始化为NULL)
- 小心指针越界
- 指针指向空间释放即使其置为NULL
- 指针使用之前检查有效性 (例如:判断是否为NULL)
3.static、const、volatile、extern关键字的作用
- static
- 修饰局部变量
- 当一个局部变量被static修饰后,它具有了全局变量的生存周期和局部变量的作用域。
- 修饰全局变量
- 称为静态全局变量与全局变量相同,都储存在数据段,编译阶段分配内存空间,在程序结束时释放。但是,对于多文件的程序来说,静态全局变量只在定义该变量的源文件或者包含该变量定义的文件内有效。
- 修饰函数
- 与全局变量类似,只能在该函数定义的文件或者包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件中 。多个文件中可以定义相同的名字,防止命名冲突。
- 修饰类的成员变量
- 用static修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例。 因此,static成员必须在类外进行初始化(初始化格式:int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。
- 修饰类的成员函数
- 静态成员函数不能调用非静态成员函数,但是非静态成员函数可以调用静态成员函数。因为静态成员函数没有对象,即:有对象可以调用没对象,没对象不能调用有对象。不能被const来修饰,
- 修饰局部变量
- const
- 要记得初始化
//const和变量类型可以互换位置,等价的 const int a = 10; int const a = 10; int i = 42; const int &r1 = i; //正确:允许将const int & 绑定到一个普通int对象上 const int &r2 = 42; //正确 const int &r3 = r1 * 2; //正确 int &r4 = r1 * 2; //错误 ,非常量不能引用常量 double dval = 3.14; const int &ri = dval; //正确 //等价于 先生成一个人临时整型变量 const int temp = dval; const int &ri = temp; int i = 42; int &r1 = i; const int &r2 = i; r1 = 0; //正确 r2 = 0; //错误 如何分辨指针常量和常量指针 一种方式是看 * 和 const 的排列顺序,比如 int const* p; //const * 即常量指针 const int* p; //const * 即常量指针 int* const p; //* const 即指针常量 还一种方式是看const离谁近,即从右往左看,比如 int const* p; //const修饰的是*p,即*p的内容不可通过p改变,但p不是const,p可以修改,*p不可修改; const int* p; //同上 int* const p; //const修饰的是p,p是指针,p指向的地址不能修改,p不能修改,但*p可以修改; 实例 //-------常量指针------- const int *p1 = &a; a = 300; //OK,仍然可以通过原来的声明修改值, *p1 = 56; //Error,*p1是const int的,不可修改,即常量指针不可修改其指向地址 p1 = &b; //OK,指针还可以指向别处,因为指针只是个变量,可以随意指向; //-------指针常量-------// int* const p2 = &a; a = 500; //OK,仍然可以通过原来的声明修改值, *p2 = 400; //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化 p2 = &b; //Error,因为p2是const 指针,因此不能改变p2指向的内容 //-------指向常量的常量指针-------// const int* const p3 = &a; *p3 = 1; //Error p3 = &b; //Error a = 5000; //OK,仍然可以通过原来的声明修改值 在实际应用中,常量指针要比指针常量用的多,比如常量指针经常用在函数传参中,以避免函数内部修改内容。 size_t strlen(const char* src); //常量指针,src的值不可改变; char a[] = "hello"; char b[] = "world"; size_t a1 = strlen(a); size_t b1 = strlen(b); 虽然a、b是可以修改的,但是可以保证在strlen函数内部不会修改a、b的内容。
- volatile
- 为什么用volatile?
如:volatile int i=10;
volatile 指出i是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取 - volatile常用在如下的几个地方:
- 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
- 多任务环境下各任务间共享的标志应该加 volatile;
- 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;
- volatile常用在如下的几个地方:
- 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
- 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
- C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
- 为什么用volatile?
- extern
1.引用同一个文件中的变量
1.引用另一个文件中的变量使用在定义前面,可以先声明 #include int func(); int main() { func(); //1 printf("%d",num); //2 return 0; } int num = 3; int func() { printf("%d\n",num); }
main.c #include int main() { //extern const int num; //也可以用const修饰 extern int num; //注意只声明即可,不需要初始化,在原文件就初始化完成了 printf("%d",num); return 0; } b.c #include int num = 5; void func() { printf("fun in a.c"); } 下面这样不行,必须是全局变量才行 b.c #include void func() { int num = 5; printf("fun in a.c"); } 直接include<b.c>也行,但是这样b.c中的所有信息都能被访问,不安全 函数使用extern同理
4.C++的四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast
-
static_cast(静态转换)
- static_cast相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性
- 注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性
#include<iostream> #include<typeinfo> using namespace std; class base { }; class derived:public base { }; int main() { base b1; derived d1; //上行转换 cout << "d1转换前的类型:" << typeid(d1).name() << endl; base b2 = static_cast<base>(d1); cout<<"d1转换后的类型:"<< typeid(b2).name() << endl; int a = 10; cout << "a转换前的类型:" << typeid(a).name() << endl; double b = static_cast<double>(a); cout << "a转换后的类型:" << typeid(b).name() << endl; system("pause"); return 0; }
-
dynamic_cast(动态转换,dynamic_cast:只可用于有虚函数的类;只可以转指针或引用)
- dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(cross cast)
- 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能(dynamic_cast也是一个RTTI运算符),比static_cast更安全。dynamic_cast是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作
#include<iostream>
#include<typeinfo>
using namespace std;
class base
{
};
class derived:public base
{
};
int main()
{
derived *d1= new derived;
cout << "d1转换前的类型:" << typeid(d1).name() << endl;
base* b1 = dynamic_cast<base*>(d1);
cout << "d1转换后的类型:" << typeid(b1).name() << endl;
delete d1;
system("pause");
return 0;
}
- const_cast
- const_cast可以修改定义为const的常量,但仅限于通过const_cast<>()语法进行修改,而不是消除了常量的const属性使得其可以在任意地方进行修改。
#include<iostream>
using namespace std;
int main()
{
const int &a = 10;
cout <<"const_cast修改之前a的值:"<< a << endl;
const_cast<int&>(a) = 5;
cout << "const_cast修改之后a的值:" << a << endl;
const_cast<int&>(a) = 20;
cout << "const_cast二次修改之后a的值:" << a << endl;
system("pause");
return 0;
}
- reinterpret_cast(用于各种危险的类型转换,尽量少用)
- 比如int转指针,指针转int
- 不允许删除const
5.inline内联函数与#define宏定义的区别
1. #define宏定义
优点:
1.加快了代码的运行效率
2.让代码变得更加的通用
缺点:
1.宏没有类型检测,不安全
2.宏是在预处理时进行简单文本替换,并不是简单的参数传递(很难处理一些特定情况。例如:Add(z++))
3.使代码变长
4.宏不能进行调试
5.当预处理搜索#define定义的符号时,字符串常量并不被搜索
2.inline内联函数
优点:
1.有类型检测,更加的安全
2.内联函数是在程序运行时展开,而且是进行的是参数传递
3.编译器可以检测定义的内联函数是否满足要求,如果不满足就会当作普通函数调用(内联函数不能递归,内联函数不能太大)
缺点:
代码变长,占用更多内存
3.总结
都比调用函数更有效率,函数的调用必须要将程序的执行顺序转移到函数所存放的内存地址中,调用完还得回复现场,开销比价大。
相同点:
两者都是可以加快程序运行效率,使代码变得更加通用
不同点:
1.内联函数的调用是传参,宏定义只是简单的文本替换
2.内联函数可以在程序运行时调用,宏定义是在程序编译进行
3.内联函数有类型检测更加的安全,宏定义没有类型检测
4.内联函数在运行时可调式,宏定义不可以
5.内联函数可以访问类的成员变量,宏不可以
6.类中的成员函数是默认的内联函数