C++模块1:基础知识点梳理

C++模块1:基础知识点梳理

1. 有符号类型和无符号类型:

当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模之后的余数。当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的;此时,程序可能继续工作、可能崩溃。也可能生成垃圾数据。
如果表达式中既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动转换成无符号数。

int a = 1;unsigned int b = -2;
cout<<a+b<<endl;   // 输出4294967295
int c = a + b;   //c=-1;
a = 3;b = -2;
cout<<a+b<<endl;   // 输出1

2 .引用与指针:

引用并非对象,它只是为一个已经存在的对象起的一个别名。在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,应用将和它的初始值绑定在一起。以为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

指针是指向另外一种类型的符合类型。与引用类似,指针也实现了对其他对象的简介访问。然而指针与引用相比又有许多不同点:

指针本身就是一个对象,允许对指针赋值和拷贝。而且在指针的生命周期内它可以先后指向几个不同的对象。引用不是对象,所以也不能定义指向引用的指针。
指针无须在定义时赋值。

==void*是一种特殊的指针类型,可以存放任意对象的地址。但我们对该地址中存放的是什么类型的对象并不了解,所以也不能直接操作void*==指针所指的对象。

3. static关键字:

申明为static的局部变量,存储在静态存储区,其生存期不再局限于当前作用域,而是整个程序的生存期。

对于全局变量而言, 普通的全局变量和函数,其作用域为整个程序或项目,外部文件(其它cpp文件)可以通过extern关键字访问该变量和函数;static全局变量和函数,其作用域为当前cpp文件,其它的cpp文件不能访问该变量和函数。

当使用static修饰成员变量和成员函数时,表示该变量或函数属于一个类,而不是该类的某个实例化对象。

4. const限定符:

const的作用:

A:在定义常变量时必须同时对它初始化,此后它的值不能再改变。常变量不能出现在赋值号的左边(不为“左值”);
B:对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
C:在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
D:对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
E:对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为"左值"。例如:

//operator*的返回结果必须是一个const对象,否则下列代码编译出错
const classA operator*(const classA& a1,const classA& a2);  
classA a, b, c;
(a*b) = c;  //对a*b的结果赋值。操作(a*b) = c显然不符合编程者的初衷,也没有任何意义

用const修饰的符号常量的区别:const位于()的左边,表示被指物是常量;const位于()的右边,表示指针自身是常量(常量指针)。(口诀:左定值,右定向)

const char *p;  //指向const对象的指针,指针可以被修改,但指向的对象不能被修改。
char const *p;  //同上
char * const p; //指向char类型的常量指针,指针不能被修改,但指向的对象可以被修改。
const char * const p;  //指针及指向对象都不能修改。

const与#define的区别

A:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
B:有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
C:在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

5 数组与指针的区别:

A:数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
B:用运算符sizeof可以计算出数组的容量(字节数)。sizeof§,p为指针得到的是一个指针变量的字节数,而不是p所指的内存容量。C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
C:C++编译系统将形参数组名一律作为指针变量来处理。实际上在函数调用时并不存在一个占有存储空间的形参数组,只有指针变量。
实参数组名a代表一个固定的地址,或者说是指针型常量,因此要改变a的值是不可能的。例如:a++;是错误的。形参数组名array是指针变量,并不是一个固定的地址值。它的值是可以改变的。例如:array++;是合法的。

为了节省内存,C/C++把常量字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。但用常量内存初始化数组时,情况却有所不同。

char str1[] = “Hello World”;
char str2[] = “Hello World”;
char *str3[] = “Hello World”;
char *str4[] = “Hello World”;

其中,str1和str2会为它们分配两个长度为12个字节的空间,并把“Hello World”的内容分别复制到数组中去,这是两个初始地址不同的数组。str3和str4是两个指针,我们无须为它们分配内存以存储字符串的内容,而只需要把它们指向“Hello World”在内存中的地址就可以了。由于“Hello World”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。

6. sizeof运算符:
sizeof是C语言的一种单目操作符,它并不是函数。操作数可以是一个表达式或类型名。数据类型必须用括号括住,sizeof(int);变量名可以不用括号括住。

int a[50];  //sizeof(a)=200
int *a=new int[50];  //sizeof(a)=4;
Class Test{
    
    int a; static double c};  //sizeof(Test)=4
Test *s;  //sizeof(s)=4
Class Test{
    
     };  //sizeof(Test)=1
int func(char s[5]);  //sizeof(s)=4;

操作数不同时注意事项:

A:数组类型,其结果是数组的总字节数;指向数组的指针,其结果是该指针的字节数。
B:函数中的数组形参函数类型的形参,其结果是指针的字节数。
C:联合类型,其结果采用成员最大长度对齐。
D:结构类型或类类型,其结果是这种类型对象的总字节数,包括任何填充在内。

类中的静态成员不对结果产生影响,因为静态变量的存储位置与结构或者类的实例地址无关;
没有成员变量的类的大小为1,因为必须保证类的每一个实例在内存中都有唯一的地址;
有虚函数的类都会建立一张虚函数表,表中存放的是虚函数的函数指针,这个表的地址存放在类中,所以不管有几个虚函数,都只占据一个指针大小。

例题:
A:下列联合体的sizeof(sampleUnion)的值为多少。

union{
    
    
    char flag[3];
    short value;
} sampleUnion;

答案:4。联合体占用大小采用成员最大长度的对齐,最大长度是short的2字节。但char flag[3]需要3个字节,所以sizeof(sampleUnion) = 2*(2字节)= 4。注意对齐有两层含义,一个是按本身的字节大小数对齐,一个是整体按照最大的字节数对齐。

B:在32位系统中:

char arr[] = {
    
    4, 3, 9, 9, 2, 0, 1, 5};
char *str = arr;
sizeof(arr) = 8;
sizeof(str) = 4;
strlen(str) = 5;

答案:8,4,5。注意strlen函数求取字符串长度以ASCII值为0为止。

C:定义一个空的类型,里面没有任何成员变量和成员函数。
问题:对该类型求sizeof,得到的结果是什么?
答案:1。
问题:为什么不是0?
答案:当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio中每个空类型的实例占用1字节的空间。
问题:如果在该类型中添加一个构造函数和析构函数,结果又是什么?
答案:还是1。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关。
问题:那如果把析构函数标记为虚函数呢?
答案:C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,指针占用4字节,因此求sizeof得到4;如果是64位机器,将得到8。

7 .四个强制类型转换:

C++中有以下四种命名的强制类型转换:

A:static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
B:const_cast:去const属性,只能改变运算对象的底层const。常用于有函数重载的上下文中。
C:reninterpret_cast:通常为运算对象的位模式提供较低层次的重新解释,本质依赖与机器。
D:dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。主要用于多态类之间的转换

一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告;但是当我们执行了显式的类型转换之后,警告信息就被关闭了。

//进行强制类型转换以便执行浮点数除法
int j = 1,i = 2;
double slope = static_cast<double>(j)/i;

//任何非常量对象的地址都能存入void*,通过static_cast可以将指针转换会初始的指针类型
void* p = &slope;
double *dp = static_cast<double*>(p);

只有const_cast能够改变表达式的常量属性,其他形式的强制类型转换改变表达式的常量属性都将引发编译器错误。

//利用const_cast去除底层const
const char c = 'a';
const char *pc = &c;
char* cp = const_cast<char*>(pc);
*cp = 'c';

reinterpret_cast常用于函数指针类型之间进行转换。

int doSomething(){
    
    return0;};
typedef void(*FuncPtr)(); //FuncPtr是一个指向函数的指针,该函数没有参数,返回值类型为void
FuncPtr funcPtrArray[10]; //假设你希望把一个指向下面函数的指针存入funcPtrArray数组:

funcPtrArray[0] =&doSomething;// 编译错误!类型不匹配
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); //不同函数指针类型之间进行转换

dynamic_cast
有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL):
A:安全的基类和子类之间转换。
B:必须要有虚函数。
C:相同基类不同子类之间的交叉转换。但结果是NULL。

class Base {
    
    
public:
int m_iNum;
virtualvoid foo(){
    
    }; //基类必须有虚函数。保持多态特性才能使用dynamic_cast
};

class Derive: public Base {
    
    
public:
char*m_szName[100];
void bar(){
    
    };
};

Base* pb =new Derive();
Derive *pd1 = static_cast<Derive *>(pb); //子类->父类,静态类型转换,正确但不推荐
Derive *pd2 = dynamic_cast<Derive *>(pb); //子类->父类,动态类型转换,正确

Base* pb2 =new Base();
Derive *pd21 = static_cast<Derive *>(pb2); //父类->子类,静态类型转换,危险!访问子类m_szName成员越界
Derive *pd22 = dynamic_cast<Derive *>(pb2); //父类->子类,动态类型转换,安全的。结果是NULL

8 .结构体的内存对齐:

内存对齐规则:

A:每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节。
B:且最终占用字节数为成员类型中最大占用字节数的整数倍。

struct AlignData1
{
    
    
    char c;
    short b;
    int i;
    char d;
}Node;

这个结构体在编译以后,为了字节对齐,会被整理成这个样子:

struct AlignData1
{
    
    
    char c;
    char padding[1];
    short b;
    int i;
    char d;
    char padding[3];
}Node;

所以编译前总的结构体大小为:8个字节。编译以后字节大小变为:12个字节。
但是,如果调整顺序:

struct AlignData2
{
    
    
    char c;
    char d;
    short b;
    int i;
}Node;

那么这个结构体在编译前后的大小都是8个字节。
那么编译后不用填充字节就能保持所有的成员都按各自默认的地址对齐。这样可以节约不少内存!一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。

9 .malloc/free 与 new/delete的区别:

A:malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请和释放动态内存。

B:对于非内部数据类型的对象而言,用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,和一个能完成清理与释放内存工作的运算符delete。
C:new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。newdelete在实现上其实调用了malloc,free函数。
D:new建立的是一个对象;malloc分配的是一块内存

猜你喜欢

转载自blog.csdn.net/weixin_40734514/article/details/109804242