赞
踩
main函数执行之前,主要就是初始化系统相关资源:
static
变量和global
全局变量,即.data
段的内容short
,int
,long
等为0,bool
为FALSE
,指针为NULL
等等,即.bss
段的内容__attribute__((constructor))
atexit
注册一个函数,它会在main 之后执行;但是alignas在某些情况下是不能使用的,具体见下面的例子:
// alignas 生效的情况 struct Info { uint8_t a; uint16_t b; uint8_t c; }; std::cout << sizeof(Info) << std::endl; // 6 2 + 2 + 2 std::cout << alignof(Info) << std::endl; // 2 struct alignas(4) Info2 { uint8_t a; uint16_t b; uint8_t c; }; std::cout << sizeof(Info2) << std::endl; // 8 4 + 4 std::cout << alignof(Info2) << std::endl; // 4
void test(int *p) { int a=1; p=&a; cout<<p<<" "<<*p<<endl; } int main(void) { int *p=NULL; test(p); if(p==NULL) cout<<"指针p为NULL"<<endl; return 0; } //运行结果为: //0x22ff44 1 //指针p为NULL void testPTR(int* p) { int a = 12; p = &a; } void testREFF(int& p) { int a = 12; p = a; } void main() { int a = 10; int* b = &a; testPTR(b);//改变指针指向,但是没改变指针的所指的内容 cout << a << endl;// 10 cout << *b << endl;// 10 a = 10; testREFF(a); cout << a << endl;//12 }
在编译器看来, int a = 10; int &b = a; 等价于 int * const b = &a; 而 b = 20; 等价于 *b = 20; 自动转换为指针和自动解引用
需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
堆 | 栈 | |
---|---|---|
管理方式 | 堆中资源由程序员控制(容易产生memory leak) | 栈资源由编译器自动管理,无需手工控制 |
内存管理机制 | 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中) | 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了) |
空间大小 | 堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大 | 栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在 编译时确定,VC中可设置) |
碎片问题 | 对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 | 对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。(看到这里我突然明白了为什么面试官在问我堆和栈的区别之前先问了我栈和队列的区别) |
生长方向 | 堆向上,向高地址方向增长。 | 栈向下,向低地址方向增长。 |
分配方式 | 堆都是动态分配(没有静态分配的堆) | 栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。 |
分配效率 | 堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。 | 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令。 |
形象的比喻 |
栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
毫无疑问是栈快一点。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
typedef
会检查数据类型。typedef char * p_char
和#define p_char char *
区别巨大。声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
相同变量可以在多处声明(外部变量extern),但只能在一处定义。
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是8
strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
return 0;
}
在16题中有提到sizeof(str)的值为8,是在64位的编译环境下的,指针的占用大小为8字节;
而在32位环境下,指针占用大小为4字节。
一个指针占内存的大小跟编译环境有关,而与机器的位数无关。
还有疑问的,可以自行打开Visual Studio编译器自己实验一番。
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。
常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。
假设数组int a[10]; int (*p)[10] = &a;其中:
包括但不限于:
语言特性
Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性强
Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题
C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成字节码,在JVM里面运行得到结果
Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现和维护上的复杂性
垃圾回收
C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放
Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题
应用场景
Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)
Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架
对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在
相同点
不同点
引申: C++和C的struct区别
C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)
C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数
C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例
编译阶段
define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用
安全性
define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
const常量有数据类型,编译器可以对其进行类型安全检查
内存占用
define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
宏不检查类型;const会检查数据类型。
宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。
static
补充一点const相关:const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。
概念区分
顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
举个例子
int a = 10;int* const b1 = &a; //顶层const,b1本身是一个常量
const int* b2 = &a; //底层const,b2本身可变,所指的对象是常量
const int b3 = 20; //顶层const,b3是常量不可变
const int* const b4 = &a; //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a; //用于声明引用变量,都是底层const
区分作用
const int a;int const a;const int *a;int *const a;
当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
class A{ public: int num1; int num2; public: A(int a=0, int b=0):num1(a),num2(b){}; A(const A& a){}; //重载 = 号操作符函数 A& operator=(const A& a){ num1 = a.num1 + 1; num2 = a.num2 + 1; return *this; }; }; int main(){ A a(1,1); A a1 = a; //拷贝初始化操作,调用拷贝构造函数 A b; b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2 return 0; }
为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++;
哪些情况下使用extern “C”:
(1)C++代码中调用C语言代码;
(2)在C++中的头文件中使用;
(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;
举个例子,C++中调用C代码:
#ifndef __MY_HANDLE_H__
#define __MY_HANDLE_H__
extern "C"{
typedef unsigned int result_t;
typedef void* my_handle_t;
my_handle_t create_handle(const char* name);
result_t operate_on_handle(my_handle_t handle);
void close_handle(my_handle_t handle);
}
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就不会直接报错,产生非法内存访问。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
产生原因及解决办法:
野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。
什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。
类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。
(1)重载(overload)
重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。举个例子:
class A{
...
virtual int fun();
void fun(int);
void fun(double, double);
static int fun(char);
...
}
(2)重写(覆盖)(override)
重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:
与基类的虚函数有相同的参数个数
与基类的虚函数有相同的参数类型
与基类的虚函数有相同的返回值类型
举个例子:
//父类
class A{
public:
virtual int fun(int a){}
}
//子类
class B : public A{
public:
//重写,一般加override可以确保是重写父类的函数
virtual int fun(int a) override{}
}
重载与重写的区别:
隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:
两个函数参数相同,但是基类函数不是虚函数。**和重写的区别在于基类函数是否是虚函数。**举个例子:
//父类 class A{ public: void fun(int a){ cout << "A中的fun函数" << endl; } }; //子类 class B : public A{ public: //隐藏父类的fun函数 void fun(int a){ cout << "B中的fun函数" << endl; } }; int main(){ B b; b.fun(2); //调用的是B中的fun函数 b.A::fun(2); //调用A中fun函数 return 0; }
两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。举个例子:
//父类 class A{ public: virtual void fun(int a){ cout << "A中的fun函数" << endl; } }; //子类 class B : public A{ public: //隐藏父类的fun函数 virtual void fun(char* a){ cout << "A中的fun函数" << endl; } }; int main(){ B b; b.fun(2); //报错,调用的是B中的fun函数,参数类型不对 b.A::fun(2); //调用A中fun函数 return 0; }
C++中的构造函数可以分为4类:
默认构造函数
初始化构造函数(有参数)
拷贝构造函数
移动构造函数(move和右值引用)
委托构造函数
转换构造函数
#include <iostream> using namespace std; class Student{ public: Student(){//默认构造函数,没有参数 this->age = 20; this->num = 1000; }; Student(int a, int n):age(a), num(n){}; //初始化构造函数,有参数和参数列表 Student(const Student& s){//拷贝构造函数,这里与编译器生成的一致 this->age = s.age; this->num = s.num; }; Student(int r){ //转换构造函数,形参是其他类型变量,且只有一个形参 this->age = r; this->num = 1002; }; ~Student(){} public: int age; int num; }; int main(){ Student s1; Student s2(18,1001); int a = 10; Student s3(a); Student s4(s3); printf("s1 age:%d, num:%d\n", s1.age, s1.num); printf("s2 age:%d, num:%d\n", s2.age, s2.num); printf("s3 age:%d, num:%d\n", s3.age, s3.num); printf("s2 age:%d, num:%d\n", s4.age, s4.num); return 0; } //运行结果 //s1 age:20, num:1000 //s2 age:18, num:1001 //s3 age:10, num:1002 //s2 age:10, num:1002
默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作
复制构造函数用于复制本类的对象
转换构造函数用于将其他类型的变量,隐式转换为本类对象
浅拷贝
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误
深拷贝
深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。
#include <iostream> #include <string.h> using namespace std; class Student { private: int num; char *name; public: Student(){ name = new char(20); cout << "Student" << endl; }; ~Student(){ cout << "~Student " << &name << endl; delete name; name = NULL; }; Student(const Student &s){//拷贝构造函数 //浅拷贝,当对象的name和传入对象的name指向相同的地址 name = s.name; //深拷贝 //name = new char(20); //memcpy(name, s.name, strlen(s.name)); cout << "copy Student" << endl; }; }; int main() { {// 花括号让s1和s2变成局部对象,方便测试 Student s1; Student s2(s1);// 复制对象 } system("pause"); return 0; } //浅拷贝执行结果: //Student //copy Student //~Student 0x7fffed0c3ec0 //~Student 0x7fffed0c3ed0 //*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 *** //深拷贝执行结果: //Student //copy Student //~Student 0x7fffebca9fb0 //~Student 0x7fffebca9fc0
从执行结果可以看出,浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。
在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。
内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。
宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义
内联函数有类型检测、语法判断等功能,而宏没有
内联函数适用场景:
使用宏定义的地方都可以使用 inline 函数。
作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
#include <iostream>
using namespace std;
int main()
{
int a = 0x1234;
//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
char c = (char)(a);
if (c == 0x12)
cout << "big endian" << endl;
else if(c == 0x34)
cout << "little endian" << endl;
}
方式二:巧用union联合体
#include <iostream> using namespace std; //union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值 union endian { int a; char ch; }; int main() { endian value; value.a = 0x1234; //a和ch共用4字节的内存空间 if (value.ch == 0x12) cout << "big endian"<<endl; else if (value.ch == 0x34) cout << "little endian"<<endl; }
class A { public: A() {}; A(const A& a) { cout << "copy constructor is called" << endl; }; ~A() {}; }; void useClassA(A a) {} A getClassA()//此时会发生拷贝构造函数的调用,虽然发生NRV优化,但是依然调用拷贝构造函数 { A a; return a; } //A& getClassA2()// VS2019下,此时编辑器会进行(Named return Value优化)NRV优化,不调用拷贝构造函数 ,如果是引用传递的方式返回当前函数体内生成的对象时,并不发生拷贝构造函数的调用 //{ // A a; // return a; //} int main() { A a1,a3,a4; A a2 = a1; //调用拷贝构造函数,对应情况1 useClassA(a1);//调用拷贝构造函数,对应情况2 a3 = getClassA();//发生NRV优化,但是值返回,依然会有拷贝构造函数的调用 情况3 a4 = getClassA2(a1);//发生NRV优化,且引用返回自身,不会调用 return 0; }
#include <iostream> #include <string> using namespace std; int main() { try { char *p = new char[10e11]; delete p; } catch (const std::bad_alloc &ex) { cout << ex.what() << endl; } return 0; } //执行结果:bad allocation
举个例子:
#include <iostream> #include <string> using namespace std; int main() { char *p = new(nothrow) char[10e11]; if (p == NULL) { cout << "alloc failed" << endl; } delete p; return 0; } //运行结果:alloc failed
#include <iostream> #include <string> using namespace std; class ADT{ int i; int j; public: ADT(){ i = 10; j = 100; cout << "ADT construct i=" << i << "j="<<j <<endl; } ~ADT(){ cout << "ADT destruct" << endl; } }; int main() { char *p = new(nothrow) char[sizeof ADT + 1]; if (p == NULL) { cout << "alloc failed" << endl; } ADT *q = new(p) ADT; //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可 //delete q;//错误!不能在此处调用delete q; q->ADT::~ADT();//显示调用析构函数 delete[] p; return 0; } //输出结果: //ADT construct i=10j=100 //ADT destruct
#include <iostream> using namespace std; int main() { double m = 1, n = 0; try { cout << "before dividing." << endl; if (n == 0) throw - 1; //抛出int型异常 else if (m == 0) throw - 1.0; //拋出 double 型异常 else cout << m / n << endl; cout << "after dividing." << endl; } catch (double d) { cout << "catch (double)" << d << endl; } catch (...) { cout << "catch (...)" << endl; } cout << "finished" << endl; return 0; } //运行结果 //before dividing. //catch (...) //finished
#include <iostream> #include <typeinfo> using namespace std; class A{ public: virtual ~A(); }; using namespace std; int main() { A* a = NULL; try { cout << typeid(*a).name() << endl; // Error condition } catch (bad_typeid){ cout << "Object is NULL" << endl; } return 0; } //运行结果:bject is NULL
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。