C语言概念总结
1引用与指针的区别:
1)引用必须初始化,指针不必;
2)引用初始化后不能被改变,指针可以改变所指的对象
3)不存在指向空值的引用,但存在指向空值的指针NULL.
指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作
2实时系统的基本特性:在特定时间内完成特定的任务,实时性与可靠性。
3平衡二叉树:左右子树都是平衡二叉树,且左右子树的深度差值绝对值不大于1。
4堆栈溢出原因:1.没有回收垃圾资源 2.层次太深的递归调用
5什么函数不能声明为虚函数:constructor
构造函数,因为它是在对象产生之前被调用的,而虚函数是对象产生之后才起作用的机制,所以声明虚构造函数无意义。
6变量与“零值”比较的if语句:
BOOL : if ( !a ) or if(a)
int : if ( a == 0)
float : const EXPRESSION EXP = 0.000001
if ( a >- EXP && a <EXP)
pointer : if ( a != NULL) or if(a == NULL)
7程序的内存分配
答:一个由c/C++编译的程序占用的内存分为以下几个部分
1.栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈,在函数返回时自动销毁。
2.堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收(使用完一定要销毁,防止内存泄露)。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3.全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
4.文字常量区—常量字符串就是放在这里的。程序结束后由系统释放。
5.程序代码区—存放函数体的二进制代码
例子程序
//main.cpp
int a=0; //全局初始化区
char *p1; //全局未初始化区
main()
{
intb;栈
char s[]="abc"; //栈
char *p2; //栈
char *p3="123456"; //123456\0在常量区,p3在栈上。
static int c=0; //全局(静态)初始化区
p1 = (char*)malloc(10);
p2 = (char*)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。
}
8堆和栈的区别
(1)申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间
heap:需要程序员自己申请,并指明大小,在c中malloc函数
如p1=(char*)malloc(10);
在C++中用new运算符
如p2=(char*)new(10);
但是注意p1、p2本身是在栈中的。
(2)申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
(3)申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
(4)申请效率的比较:
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用Virtual Alloc分配内存,它不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
(5)堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
9malloc与new的区别:
1.malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2.对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3.因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4.C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
5.new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针
6.new自动计算需要分配的空间,而malloc需要手工计算字节数
7.new是类型安全的,而malloc不是,比如:
int* p = new float[2]; // 编译时指出错误
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
8.内存泄漏对于malloc或者new都可以检查出来的,区别在于new可以指明是那个文件的那一行,而malloc没有这些信息。
10构造函数与析构函数
构造函数(constructor)是类的一个特殊的成员函数,它与类名同名。当定义该类的对象时,构造函数将被系统自动调用用以实现对该对象的初始化。
构造函数不能有返回值,因而不能指定包括void在内的任何返回值类型。
构造函数的定义与其他成员函数的定义一样可以放在类内或类外。
构造函数的定义格式为:
类名(形参说明)
{函数体}
构造函数既可以定义成有参函数,也可以定义成无参函数,要根据问题的需要来定。
注意:程序中不能直接调用构造函数,构造函数是在创建对象时由系统直接调用的,因此,在构造函数中一般完成初始化类成员变量的操作。
析构函数
与构造函数对应的是析构函数。当一个对象被定义时,系统会自动调用构造函数为该对象分配相应的资源,当对象使用完毕后且在对象消失前,系统会自动调用类的析构函数来释放这些系统资源。
析构函数也是类的一个特殊的成员函数,其函数名称是在类名的前面加上“~”;它没有返回值,也没有参数。一个类中只能拥有一个析构函数,所以析构函数不能重载。
析构函数的定义方式为:
~类名()
{ 函数体 }
如果程序员在定义类时没有为类提供析构函数,则系统会自动创建一个默认的析构函数,其形式为:
~类名()
{ }
对象被析构的顺序与其创建时的顺序正好相反,即最后构造的对象最先被析构。
如果一个对象是被new运算符动态创建的,当使用delete运算符释放它时,delete将会自动调用析构函数。
11函数重载
指同一个函数名可以对应着多个函数的实现。每种实现对应着一个函数体,这些函数的名字相同,但是函数的参数或类型不同。这就是函数重载的概念。函数重载要求编译器能够唯一地确定调用一个函数时应执行哪个函数代码,即采用哪个函数实现。确定函数实现时,要求从函数参数的个数和类型上来区分。这就是说,进行函数重载时,要求同名函数在参数个数上不同,或者参数类型上不同,编译器根据实参和形参的类型及个数进行最佳匹配,自动确定调用哪个函数,否则,将无法实现重载。
12函数重入
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。
13预编译
预编译又称为预处理,是做些代码文本的替换工作。处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。
C编译系统在对程序进行通常的编译之前,先进行预处理。c提供的预处理功能主要有以下三种:1)宏定义#define 2)文件包含#include 3)条件编译#if、#else和#endif
1. 总是使用不经常改动的大型代码体。
2.程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个预编译头。
13内联函数与宏定义区别
(1)内联函数在编译时展开,宏在预编译时展开;
(2)内联函数直接嵌入到目标代码中,宏是简单的做文本替换;
(3)内联函数有类型检测、语法判断等功能,而宏没有;
(4)inline函数是函数,宏不是;
(5)宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义。
14const与宏常量的区别:
1)const常量有数据类型,而宏常量没有数据类型
编译器可以对前者进行类型安全检查,而对后者只能进行字符替换,没有安全检查,并且在字符替换时可能会产生意料不到的错误。
2)编译器对二者的调试
有些集成化的调试工具可以对const常量进行调试, 在 c++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
15数组与指针的区别
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
char a[] = “hello”;
char *p = “world”; // 注意p 指向常量字符串
(1)修改内容上的差别
a[0] = ‘X’;
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
(2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个 指针变量的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
sizeof(a)//6字节
sizeof(p)//4字节
16define和typedef有以下四点区别:
1.用法不同
typedef用来定义一种数据类型的别名,增强程序的可读性;
define主要用来定义常量,预计书写复杂且使用频繁的表达式。
2.执行时间不同
typedef未分配内存空间,在预处理时被替换,且有类型检查的功能;
define是宏定义,在预处理时被替换,是简单的字符串的替换,不进行类型的检查。
3.作用域不同
typedef有作用域的限制;
define没有作用域的限制,只要是在define声名后的引用都正确。例如:
4.对指针的操作不同
例如:#define pCHAR char* 和 #typedef pCHAR char* 是两种概念
17C和C++有什么不同?
从机制上:c是面向过程的(但c也可以编写面向对象的程序);c++是面向对象的,提供了类。但是,
c++编写面向对象的程序比c容易
从适用的方向:c适合要求代码体积小的,效率高的场合,如嵌入式;c++适合更上层的,复杂的; llinux核心大部分是c写的,因为它是系统软件,效率要求极高。
从名称上也可以看出,c++比c多了+,说明c++是c的超集;那为什么不叫c+而叫c++呢,是因为c++比
c来说扩充的东西太多了,所以就在c后面放上两个+;于是就成了c++
C语言是结构化编程语言,C++是面向对象编程语言。
C++侧重于对象而不是过程,侧重于类的设计而不是逻辑的设计。
18 0,'0',"0",'\0'的区别。
分别为数值0,字符0(ascii码数值为48),字符串0(占两个字节,有‘\0’作为结束符),字符串结束标志(其数值等于0是绝对的0,但意义不同)
19指针 函数 数组
a)一个整型数(An integer)
int a; // An integer
b) 一个指向整型数的指针(A pointer to an integer)
int *a; // A pointer to an integer
c) 一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)
int **a; // A pointer to a pointer to an integer
d) 一个有10个整型数的数组(An array of 10 integers)
int a[10]; // An array of 10 integers
e) 一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers to integers)
int *a[10]; // An array of 10 pointers to integers
f) 一个指向有10个整型数数组的指针(A pointer to an array of 10 integers)
int (*a)[10]; // A pointer to an array of 10 integers
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
20地址对齐
现代计算机中内存空间都是按照字节(byte)划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排列,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定的类型的数据只能从某些特定的地址开始存取。其它平台可能没有这些限制,但是最常见是的如果不按照适合其平台的要求对数据存储进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶数地址开始,如果一个 int 型(假设是 32 位)如果存放在偶数地址开始的地方,那么一个时钟周期就可以读出。而如果是存放在一个奇数地址开始的地方,就可能会需要 2 个时钟周期,并对两次读出的结果的高低字节进行拼凑才能得到该 int 型数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
分为:字节对齐,半字对齐,字对齐
21 assert()宏用法
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言,而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新起用断言
assert的作用是如果它的条件返回错误,则终止程序执行,先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。
使用assert的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
用法总结与注意事项:
1)在函数开始处检验传入参数的合法性
每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败
3)不能使用改变环境的语句,因为assert只在DEBUG时生效,如果这么做,会使用程序在真正运行时遇到问题
4)assert和后面的语句应空一行,以形成逻辑和视觉上的一致感
22叶子函数:指内部不会再调用其他函数的函数。