private: 私有成员,只有本类内的成员函数可以访问;
public: 公有成员,类作用域中的所有函数都可以访问;
protected: 受保护的成员,类外不可以访问,派生类可以访问。
private、public、protected这几个访问限制说明符的作用就是要实现C++类的“封装性”—— 将私有的数据成员声明为private,这部分数据对外部对象不可见,而public则是类为外部使用者提供的访问类内数据的方式的接口,想要访问类的私有成员,必须通过类指定的public接口函数。
class Sales_data {
public:
std::string ShowIsbn() { std::cout << isbn; }
double ShowPrice() { std::cout << price; }
private:
std::string isbn;
double price;
};
例如对于上面的一个用于描述商品信息的类,每个商品都是一个实例化的Sales_data类型的对象,类外使用者只能通过ShowIsbn()函数和ShowPrice()函数查看商品价格,想要擅自修改商品价格是不行的。
public公有继承: 基类的公有成员和受保护成员在派生类中保持原有的访问属性,基类的私有成员仍然为基类的私有;
private私有继承: 基类的公有成员和保护成员在派生类中成了私有成员,基类的私有成员仍然为基类私有;
protected保护继承: 基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍然为基类私有。
总之,无论是三种继承方式中的哪一种,被继承的基类中的私有成员永远都为基类私有,访问这部分成员必须通过基类的public接口函数,即使是在派生类内想要访问基类的私有成员,也必须通过基类指定的方式,即基类的public接口函数。这符合类的封装性。
函数的调用涉及到堆栈的操作,对于一些频繁被调用的小规模函数,函数调用切换的开销要远远大于顺序执行的开销,对于这类函数可以将其声明为inline内联函数,在编译时将其进行展开。
在类内定义的成员函数,且其中不包含循环、递归、条件等复杂操作时,编译器会默认将其当作内置函数(inline)来处理。
例如:
class Sales_data {
public:
//类内定义,是inline内联函数:
std::string showIsbn() { std::cout << isbn; }
//类内声明,类外定义,不是inline函数:
double showPrice();
};
double Sale_data::showPrice() {
std::cout << price;
}
编译器对inline函数的处理步骤:
(1)将inline函数体复制到inline函数调用点处;
(2)为所用inline函数中的局部变量分配内存空间(栈空间);
(3)将inline函数的输入参数和返回值映射到调用方的局部变量空间中;
(4)如果inline函数有多个返回点,将其转变为inline函数代码块末尾的分支(使用goto)。
优缺点:
优点:
(1)内联函数同宏函数(#define)一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收、结果返回 等步骤,从而提高了程序的运行速度;
(2)内联函数相比于宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏函数则不会;
(3)在类中定义声明的成员函数,自动转换为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能;
(4)内联函数在运行时可调试,而宏定义则不能。
缺点:
(1)代码膨胀。 每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间;
(2)inline函数无法随着函数库的升级而升级。inline函数的改变需要重新编译,不能向non-inline可以直接链接;
(3)是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在编译器手里。
C语言中是没有类似与C++、Java等语言中的string那样的特定的字符串类型的,C语言是将字符串放进一个字符数组中。
对于字符数组的初始化,有两种形式:
//(1)用 "字符常量" 逐个初始化数组中的元素:
char array[] = {'h', 'e', 'l', 'l', 'o'};
//(2)用 "字符串常量" 初始化整个数组:
char array[] = "hello"; //初始化后的结果:array[] = {'h', 'e', 'l', 'l', 'o', '\0'};
注意第二种使用“字符串常量”初始化字符串数组的方式,会在数组尾部加上一个 ‘\0’ 字符。
字符串常量在程序的内存布局中是存放在常量区的,字符数组在被初始化时,相当于是字符串常量的一个副本(存在在栈区或堆区)。
另外,字符串数组与字符串指针 之间也存在很大的差别:
char amessage[] = "now is the time"; //定义一个数组
char *pmessage = "now is the time"; //定义一个指针
上述声明中,amessage是一个仅仅足以存放初始化字符串以及空字符’\0’的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。
另一方面,pmessage是一个指针,其初值指向一个字符串常量,指针的指向可以修改,但如果试图通过指针来修改字符串的内容,结果是未定义的。
strlen和sizeof可以用来计算字符串的长度:
strlen: 从内存的某个位置开始扫描,直到碰到第一个字符串结束符’\0’为止,然后返回计数器值(长度不包含’\0’);
sizeof: 对数组,计算得到整个数组所占空间的大小;
对指针,得到指针本身所占空间的大小。
示例:
#include
int main() {
char array[] = {'h','e','l','l','o'};
char arr[] = "hello";
char *str = "hello";
std::cout << sizeof(array); //5 计算的是数组的大小
std::cout << sizeof(arr); //6
std::cout << sizeof(str); //8
std::cout << strlen(array); //?? 末尾没有'\0',strlen得到的值随机
std::cout << strlen(arr); //5
std::cout << strlen(str); //5
return 0;
}
------
输出结果:
5
6
8
5
5
5
验证 字符串常量 与 字符串数组 在内存布局中的存储位置:
#include
int main() {
char a = 'c';
char *atr = &a;
char *ptr = "hello";
printf("%p, %p\n\n\n", atr, ptr);
}
------
0x7ffee24f9a6b, 0x10d709f74
“字符串指针”就把它理解成一个指向常量区的指针,它的指向可以变,但是当它指向常量区时不能所指对象的值。
*atr指向的栈空间,*ptr指向的是常量区,二者的地址差距巨大。
联合(union)是一种节省空间的特殊的类,一个union可以有多个数据成员,共用体中的所有成员共享同一个空间,并且同一时间只能储存其中一个成员变量的值。
同struct一样,union 的默认访问权限也是public公有的,并且也可以在union 内声明/定义成员函数。
预编译: 头文件替换、宏定义展开等预处理命令替换;(.c/.cpp -> .i)
编译: 源文件语言编译成汇编语言;(.i -> .s)
汇编: 把汇编语言翻译成目标机器指令;(.s -> .o)
链接: 链接目标文件生成可执行文件。(.o -> .out)
(1)引用是变量一个别名,内部实现是 只读指针;
(2)引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变;
(3)引用不能为NULL,指针可以为NULL;
(4)引用变量内存单元保存的是被引用变量的地址;
(5)“sizeof引用”=指向变量的大小,“sizeof指针”=指针本身的大小;
(6)引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址;
(7)引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量的地址。
先申请一定大小的数组,当数组填满之后,另外申请一块原数组两倍大小的新数组,然后把原数组的数据拷贝到新数组,最后释放原数组的内存。
#include
#include
using namespace std;
int main() {
vector<int> v;
for(int i = 0; i < 10; i++) {
cout << i << ' ' << v.size() << ' ' << v.capacity() << endl;
v.push_back(i);
}
return 0;
}
------
0 0 0
1 1 1
2 2 2
3 3 4
4 4 4
5 5 8
6 6 8
7 7 8
8 8 8
9 9 16
vector<int> v;
list<int> l;
cout << (v.begin() < v.end()) << endl;
cout << *(v.begin() + 1) << endl;
cout << v[2] << endl;
//++操作
for(vector<int>::iterator itv = v.begin(); itv != v.end(); itv++) {
}
for(vector<int>::iterator itl = l.begin(); itl != l.end(); itl++) {
}
在C++中,有五种函数不能被声明为虚函数,分别是:
非成员函数、构造函数、静态成员函数、内联成员函数、友元函数。
总之一个原则,C++引入虚函数的目的是为了实现运行时多态,使用基类的指针分别调用基类和派生类中的virtual虚函数时可以产生不同的结果。因此,没有继承特性的函数,就没有虚函数的说法。
(1)非成员函数:
虚函数针对的是类内的成员函数,非成员函数只能被重载(overload),不能被继承(override)。非成员函数在编译期间就已经绑定函数了。
(2)构造函数:
要想调用虚函数,必须要通过类的“虚函数表”进行,而虚函数表是要在对象实例化之后才能够调用(虚函数表指针(vptr)是含有虚函数的类的隐式成员,类对象在初始化时,构造函数负责初始化虚函数表指针指向类的虚函数表(virtual function table)地址)。
在构造函数运行期间,还没有为虚函数表分配空间,自然就没法调用虚函数了。
(3)静态成员函数:
静态成员函数对于每个类来说只有一份,所有的对象都共用这一份代码,它是属于类的而不是属于对象。虚函数必须根据对象类型才能知道调用哪一个虚函数,故虚函数是一定要在对象基础上才可以的,两者一个是与实例相关,一个是与类相关。
(个人理解,static静态成员函数是在编译期确定,函数地址保存在代码区中,而虚函数是需要放在虚函数表中,在编译时被动态绑定的,C++在实现时并没有把静态函数放在虚函数表中,而是放在了静态区)
(4)内联成员函数:
内联函数会在编译时被展开,虚函数在运行时才能动态的绑定函数。
实际上,virtual和inline可以共存,编译时可以通过,inline只是对编译器的一个建议,至于是否真的将这个函数作为inline,需要根据汇编语言查看。
#include
using namespace std;
class Base {
public:
virtual inline void print() { cout << "base func" << endl; }
};
class Derived : public Base {
public:
virtual inline void print() { cout << "derived func" << endl; }
};
int main() {
Base b;
Derived d;
Base *ptr = &b;
ptr->print();
ptr = &d;
ptr->print();
return 0;
}
------
base func
derived func
(5)友元函数:
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。并且友元函数也不属于类的成员函数,不能被继承。
C++中的class类默认的成员是私有的,struct默认的是公有的。
注意:
网上有人说 “C++中的class类可以定义成员函数, struct只能定义成员变量”,这种说法是错误的:在C++中,struct结构体中同样可以定义成员函数,同样可以通过 访问说明符(access specifiers)public、private 来实现封装。
#include
using namespace std;
//用struct关键字来实现一个类:
struct Demo {
public:
void print() {
cout << "hello" << endl;
}
private:
int a;
};
int main() {
struct Demo dm;
dm.print();
return 0;
}
------
hello
在《C++ Primer》一书中7.2节P240中有关于class与sruct的描述:
使用class或strut关键字:
在上面的定义中我们还做了一个微秒的变化,我们使用了class关键字而非struct开始类的定义。这种变化仅仅是形式上有所不同,实际上我们可以使用这两个关键字中的任何一个定义类。唯一的一点区别是,struct和class的默认访问权限不太一样。
类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的。
使用class和struct定义类唯一的区别就是默认的访问权限。
声明变量不分配空间,定义变量要分配空间。
声明主要是告诉编译器,后面的引用都按声明的格式。定义其实包含了声明的意思,同时要分配内存空间。
在理解堆和栈这两个概念时,需要放到具体的场景下。一般情况下,有两层含义:
(1)程序内存布局 场景下,堆与栈表示的是 两种 内存管理方式;
(2)数据结构 场景下,堆与栈表示两种常用的数据结构。
栈: 由操作系统自动分配、释放,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈;
堆: 由程序员分配、释放,若程序员没有显式释放,则在程序结束时由操作系统回收,分配方式类似于链表。
二者的中都是一个地址,数组名表示的是数组首地址的指针,但这个指针很特别:
(1)数组名只能指向数组的首地址,它不能被移动,例如有 char a[]; ,如果出现 a = a+1 这种移动指针的操作将会编译不通过;
(2)当使用数组名这个指针的时候,系统会传入数组的信息,例如对数组名求sizeof得到的是整个数组的长度,而对指针求sizeof的结果是指针本身的长度:
char a[5];
char *p = a;
cout << sizeof(a); //5
cout << sizeof(p); //4
面向对象程序设计(OOP)的核心思想是:数据抽象、继承、动态绑定。(或者说:封装、继承、多态)
封装,可以将类的接口与实现分离;
继承,可以定义相似但不完全相同的类型,一定程度上实现了代码复用;
多态,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象,同样是实现了代码复用,同样的一套代码实现不同的效果。
三者描述的都是“函数名相同”的多个函数之间的关系。
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为 “重载函数”(overload)。
当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。
overload重载只是一种语言特性,与继承无关、与多态无关、与面向对象无关。
main函数不能被重载。
举例:
void print(const string &);
void print(double);
void print(int);
如果两个函数的函数名、形参列表 都相同,只有二者返回值类型不同,这种情况是不被允许的,第二个函数的声明将会是错误的:
int lookup(const Account& );
bool lookup(const Account& ); //错误:与上一个函数相比只有返回值类型不同
函数的重载不区分函数的返回值类型,即只要 函数名相同、函数形参列表不同,就可以构成函数重载,与函数的返回值类型是否相同无关:
void func(int a) {}
int func(int a, int b) {}
//正确
overload重载、 overwrite重写、 override覆盖 这些都是C++语言的特性,C语言连最简单的重载都是不被允许的,因为C语言中不支持 name mangling,所以不允许函数同名,做不到重载。
所谓的“name mangling”,目的就是为了给重载的函数不同的签名,以避免调用时的二义性调用。基本支持函数重载的语言都需要进行name mangling。
大体的做法就是在编译时,在函数名的基础上再附加参数名、类型等额外的信息,用以区别源程序中同名的函数,例如:
double sum(double x, double y) {
}
void foo(int lhs, int rhs) {
}
------
编译后:
00000022 T __Z3fooii
00000000 T __Z3sumdd
重写的本质是子类成员函数屏蔽了父类成员函数。
是指派生类的函数 屏蔽了 与其同名的基类函数,规则如下:
举例:
class Base {
public:
void f1(int);
virtual void f2(int);
void f3(int);
virtual void f4(int);
};
class Derived : public Base {
public:
void f1(int, int); //重写f1
virtual void f2(int, int); //重写f2
void f3(int); //重写f3
virtual void f4(int); //override覆盖f4
};
override关键字 的真正作用:
显式的声明派生类的虚函数的目的是覆盖基类中的某个同名虚函数,避免笔误等原因造成覆盖失败,导致程序出错。
对于虚函数的覆盖,正确的写法是在派生类中声明一个与基类中的虚函数 函数名字、形参列表 都完全相同的函数。但是!派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这是,派生类的函数并没有覆盖掉基类中的版本。
就实际的编程习惯而言,这种声明往往意味着发生了错误,因为我们原本希望派生类能覆盖掉基类中的虚函数,但是一步小心把形参列表弄错了。
要想调试这样的错误非常困难,C++11标准中我们可以使用override关键字来说明派生类的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
举例:
class B {
public:
virtual void f1(int) const;
virtual void f2();
void f3();
};
class D : public B {
public:
void f1(int) const override; //正确:f1与基类中的f1匹配
void f2(int) override; //错误:B没有形如f2(int)的函数
void f3() override; //错误:f3不是虚函数
void f4() override; //错误:B没有名为f4的函数
};
#include
using namespace std;
class B {
public:
virtual void func(void) {}
};
class D : public B {
public:
virtual int func(void) { return 1; }
};
int main() {
return 0;
}
上面的例子中,func函数在派生类与基类中的函数名、形参列表相同,构成了虚函数覆盖的条件,但二者的返回值类型不同,此时编译将报错:
error: virtual function 'func' has a different return type ('int')
than the function it overrides (which has return type 'void')
virtual int func(void) {
如果func()函数前不加virtual限制(此时func是一个普通函数,而不是虚函数),则编译可以通过,此时发生的是函数的重写(overwrite),而不是覆盖(override)。
有时候对于某个类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。
C++11标准中提供了一个关键字:final,用于防止继承的发生。
class NoDerived final {
}; //NoDerived不能作为基类,其他类不能继承它
class Base {
};
class Last final : public Base {
}; //Last继承自Base,但它不允许别人继承自己
默认参数指的是当函数调用中省略了实参时自动使用的一个值。
C语言中没有参数默认值的概念,可以利用宏来模拟参数默认值;
在C++中,可以为参数指定默认值。在函数调用时没有指定与形参相对应的实参时,就自动使用默认参数。
默认参数的语法与使用:
举例:
int mal(int a, int b, int c = 6, int d = 8, int e = 10); //默认参数定义的顺序是自右向左,第一个参数c定义了缺省值,则c右边的所有参数d、e都必须定义缺省值
int main() {
mal(1, 2, 3); //等价于: mal(1, 2, ,3 8, 10);
//函数调用时,实参与形参结合的顺序是 自左向右
}
函数原型:
#include
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);
malloc用于申请size大小的堆内存,并返回指向这块内存的指针;
使用malloc申请的内存未被初始化,必须由用户使用memset对这块内存进行初始化;
malloc如果申请内存失败,则返回一个空指针;
calloc函数用于申请 nmemb 个 大小为size字节的连续内存(总大小为 (nmemb*size) byte),
使用calloc函数申请的内存是已经被系统初始化为0的;
calloc适合为数组申请内存。
realloc函数将参数ptr所指向的内存的大小调整为size字节,原来的内存中的内容将被保留到新申请的内存中;
如果新内存 < 旧内存,则 旧内存中多余的数据将被截断丢弃;
如果新内存 > 旧内存,则 新内存中多余的部分将是未被初始化的;
realloc传入的参数ptr指针必须是之前调用malloc、calloc、realloc返回的指针;
free用户释放一块堆内存,传入的指针参数ptr必须是之前使用 malloc、calloc、realloc申请的内存;
对一个块内存调用free释放两次的行为是未定义的(大部分程序的结果是core dump,理由是已经归还系统的内存可能已经被分配给其他程序使用,本程序再次调用free可能会释放一块正在被他人使用的内存,系统core dump加以防护);
对null空指针调用free是允许的。
在C++中,迭代器是类,虽然是封装了指针的类,但是绝对不能直接拿来当指针用,类似 if(iter)
或者 if(iter == nullptr)
的写法都会导致编译出错。
当需要判断查找的结果时,正确的写法是判断find的返回值是否等于 尾后迭代器(end())。
例如:
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
set<int> s_;
for(auto it = nums.begin(); it != nums.end(); ++it) {
if(s_.find(*it) != s_.end()) //判断set中是否存在某元素,切记迭代器不能直接与NULL进行比较!
return *it;
s_.insert(*it);
}
return -1;
}
};
虚函数表的第一项是什么?第二项是什么?
virtual table 的第一个slot 存储的是 type_info object;
的地址(一个 class type_info 类型的对象object),用于支持RTTI(Runtime Type Identification,运行时类型识别),在执行期获取class object的内部实现是这样的:
((type_info*)(pt->vptr[0]))->_type_descriptor;
//获取类的虚函数表中的slot 1上的内容(pt->vptr[0]),
//然后将其强转成 (type_info*) 类类型的指针,
//最后获取 type_info对象中的数据成员 _type_descriptor,即对应pt所指类的类型
virtual table 的第二个slot 存储的是 destructor
指向类的虚析构函数指针。
从virtual table 的第三个slot 开始存储的才是类中的普通的 virtual虚函数指针。
大小端字节序,指的是数据在内存中的存储方式。
大端模式(Big-Endian): 数据的高字节 保存在 内存的低地址中,数据的低字节保存在 内存的高地址中;
小端模式(Little-Endian): 数据的的高字节 保存在 内存的高地址中,数据的低字节 保存在 内存的低地址中。
以 unsigned int value = 0x123456;
为例,其分别在两种字节序下的存储情况是:
网络序:
TCP/IP协议规定,网络数据流采用大端字节序。
主机序:
与CPU有关,X86架构的CPU全部是小端字节序,ARM架构的CPU也是小端字节序。
所以,对于X86、ARM的主机,当向网络中发、收数据时,必须要先进行字节序转换。
写一个C程序辨别系统的大小端:
使用union联合体:
#include
typedef union Demo {
unsigned short value;
char array[2];
} Demo;
int main() {
Demo d;
d.value = 0x1234;
if(d.array[0] == 0x12 && d.array[1]) {
printf("big endian\n"); //高地址存高位字节
}
else {
printf("little endian\n");
}
return 0;
}
------
little endian
#include
int main() {
int num = 0x12345678;
char *ptr = (char*)#
if(*ptr == 0x78) { //低地址存高位
printf("little endian\n");
}
else {
printf("big endian\n");
}
return 0;
}
------
little endian
扩展:
C语言中用于分配内存的函数:malloc/calloc/realloc 之间的区别:
#include
void* malloc(size_t size);
void* calloc(size_t count, size_t size);
void* realloc(void* ptr, size_t size)
------
char* ptr = (char*)malloc(sizeof(char) * 2);
char* ptr = (char*)calloc(2, sizeof(char));
char* p = (char*)realloc(ptr, 3*sizeof(char));
三者都是从堆上申请内存,成功则返回指向所分配的堆内存的指针,失败则返回NULL。
区别在于:
(1)malloc申请的内存是没有被初始化的,申请后必须再通过memset进行初始化;
(2)calloc申请的内存是已经被初始化为0的,从堆上申请 nsize 字节的空间,适合为数组申请空间;
(3)realloc比较特殊,它重新分配参数ptr所指的空间为n个字节,如果新申请的大小n 不大于(<=)原有空间的大小,则保持不变;若n大于(>)原有空间的大小,则重新分配堆上的一块内存,并复制原有内容到新分配的堆上的存储空间。
注意无论使用哪种申请堆内存的方式,最后都要使用free()函数释放内存。
通常所说的 32位/64位,指的是CPU GPRS(General Purpose Registers,通用寄存器)的数据宽度。64位指令集就是运算64位数据的指令,也就是说处理器一次可以运算64 bit数据。
从32位到64位使寻址范围、最大内存容量、数据传输、处理速度、数值精度等指标都成倍的增加,带来的结果就是CPU的处理能力得到大幅提升。
在32位和64位机器上各数据类型长度的对比:
64位平台下:
注意在32位和64位平台下,char型长度都是1,short型长度都是2,int型长度都是4,long型从4变为8,指针类型长度从4变为8。
#include
int main() {
int *ptr;
printf("sizeof(ptr) = %u\n", sizeof(ptr));
return 0;
}
------
如果是32位系统,则sizeof(ptr) = 4
如果是16位系统,则sizeof(ptr) = 2
#include
int main() {
int i = 32768;
printf("i = %d\n", i);
return 0;
}
------
如果i的值为-32768,则是16位系统
如果i的值是32768,则是32位系统
根据int型的bit位数进行判断的衍生方法还有很多:
#include
int main() {
int i = 65536;
printf("i = %d\n", i); //16位系统输出0, 32位系统输出65536
int j = 65535;
printf("i = %d\n", j); //16位系统输出-1, 32位系统输出65535
return 0;
}
反码:原码按位取反
补码:反码+1,也就是 原码的反码再加一
正数的反码是其本身,正数的补码是其本身;
负数的补码是其原码取反加一。
在系统中的数组都是以 补码 的形式存放。
以16 bit整数为例(32位系统下的short型),如果是有符号数(signed short),表示的数值范围是 (-215 ~ 215 - 1 ),即 ( -32768 ~ +32767);如果是无符号数(unsigned short),表示的数值范围是 (0 ~ 216 - 1),即 (0 ~ 65535)。
为什么有 - 32768:
以 4bit位为例:
24 = 16,即4bit为的整数一共可以表示16个数字,按理说应该整数负数各占一半,但是数字0非正非负,这就造成正数负数的比例不是1:1,C语言选择负数多一个:
0000 表示0
0111 表示 +7,为正数的最大值
1111 表示 -7,(最高一位是符号为,表示负数)
1000:表示 -8
所以 4bit位的整数的取值范围是 (-24 ~ 24 - 1)。
不是,i++的实现步骤简单描述如下:
(1)读取i到某个寄存器X;
(2)X++;
(3)将更新后的X的内容存回i。
只有单条指令的操作才是原子的(Atomic),因为无论如何单条指令的执行是不会被打断的。
但是自增操作(i++)在编译为 汇编代码 之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了,去执行别的代码了。
C语言中的指数运算需要用到 pow()
函数,例如:2的4次方的写法是:
#include //注意头文件
pow(2, 4);
struct结构体“字节对齐”到底是怎样的一种“对齐”?
以结构体成员中占用内存最多的数据类型所占的字节数为标准,所有的成员在分配内存时都要与这个长度对齐。
填充规则: 什么时候另起一行?什么时候紧接着填充?
理论上所有成员在分配时都是紧接在前一个变量后面依次填充的,但是如果是“以N对齐”为原则,那么当一行中剩下的空间不足以填充某成员变量时,则该成员变量在分配内存时另起一行分配。
举例:(32位机器中)
struct {
char a;
double b;
int c;
} demo;
sizeof(demo) = 24;
//sizeof(char) = 1; sizeof(double) = 8; sizeof(int) = 4;
从一道小题开始:
下列哪个操作可以不需要在内核态执行?
A. 系统调用
B. malloc/free
C. 软中断
D. 内存换页
答案:B。 malloc/free是库函数。
C++中的“多态”,实际修饰的是“基类类型的指针或引用” ---- 使“基类的指针&引用”具有“多种形态”,即当使用基类类型的指针分别调用基类类型或派生类型对象中的虚函数时,可以产生不同的效果,目的是为不同的数据类型实体提供统一的接口,实现同一份代码的复用。
具体实现上,在编译阶段,当编译器发现某个class类中含有virtual虚函数时,① 首先,编译器会为这个类构建一个虚函数表,虚函数表,虚函数表中存储这个类中的每一个虚函数地址指针;② 其次,编译器会在每个含有虚函数的类中插入一个隐式的数据成员,即vptr虚函数表指针,在类对象初始化时,由类的构造函数负责将vptr初始化为虚函数表地址。
派生类中的虚函数可能有三种版本:① 直接继承基类的虚函数版本; ② 重新改写覆盖基类的版本;③ 纯虚函数。
在编译阶段,当通过基类指针&引用调用派生类中的虚函数时,编译器可以通过派生类对象中的vptr虚函数表地址找到对应的派生类虚函数表,以及虚函数地址指针,完成函数的调用,但是此时派生类中的虚函数是三个版本中的哪一个,在编译阶段无法确定,只能在程序运行时确定,这就是运行时多态。
#define CYCLE 5
const int cycle = 5;
#define PI 3.14 //预处理后,占用代码段空间
const float PI = 3.14; //占用数据段空间(局部变量占用栈,全局变量占用静态区)
当const用于修饰函数时,分两种情况:
const用于修饰函数也包括两种情况:
遗留:const修饰指针的实现原理是什么?
单例模式:
工厂模式:
适配器模式:
观察者模式:
装饰器模式:
策略模式:
以32位操作系统为例:
在32位的操作系统中,地址总线是32位,最大寻址能力是 232 = 4G,所以进程的虚拟地址空间的大小是 4G,即 0x00000000 到 0xffffffff。
其中,内核空间占用 1G,Linux默认情况下会将高地址的 1GB空间分配给内核,即 0xc0000000 到 0xffffffff。 用户使用的空间为3G,即 0x00000000 到 0xc0000000,其中又分为:栈空间、堆空间、静态区。
静态区又可细分为:读写段(全局变量、静态局部变量)、只读段(常量区) 和 代码段。
常用表示内存字节数的十进制/十六进制换算:
2^10 = 1024 = 1K = 0x 400
2^20 = 1024*1024 = 1M = 0x 10 0000
2^30 = 1024*1024*1024 = 1G = 0x 4000 0000
2^32 = 1024*1024*1024*1024 = 4G = 0x1 0000 0000
对于32位系统,一个进程的虚拟地址空间大小为4G,其中高地址的1G留给内核使用,用户态的虚拟地址空间大小为3G,Linux下的线程默认的栈空间大小为 8M(可以手动修改大小),那么一个进程最多能创建 300个左右的线程;
对于64位系统,用户态的虚拟地址空间大小为 128T(内核态的虚拟地址空间大小也是128T,其余空间未定义)。如果只按照虚拟内存大小计算,那么将可以创建 (128T/8M = 1000万+) 个线程,但实际上除了虚拟内存的限制,还有系统的限制。
例如如下三个内核参数,都会影响创建线程的上限:
/proc/sys/kernel/threads-max //表示系统支持的最大线程数,默认值 14553
/proc/sys/kernel/pid_max //表示系统全局的PID号数值的限制,默认值 32768,当进程或线程PID超过这个数值时将会创建失败
/proc/sys/vm/max_map_count //表示一个进程可以用于的VMA(虚拟内存区域)的数量,默认值 65530
8192 Kbytes = 8M 字节。
虚拟地址可能相同,物理地址不可能相同。
Segment Fault直译为“段错误”,指的是程序访问的内存超过了系统分配给这个进程的内存空间。
实现上,每个进程所拥有的内存空间的大小由一个 GDTR
48位寄存器保存,一旦一个程序发生了越界访问,CPU就会产生相应的异常保护,于是segment fault就出现了。
在发生段错误后,系统会向进程发送 SIGSEGV
信号。
段错误就是访问了不可访问的内存,这个内存要么是不存在的,要么是系统保护的。
在C/C++程序中,内存管理的大部分工作都是要程序员来完成的。内存管理是一个比较繁琐的工作,无论你多高明,经验多丰富,难免也会犯一些小错误。
使用gdb可以快速的定位一些“段错误”的语句,而对于大型程序,如何跟踪并找到程序中的错误位置就是需要掌握的一门技巧。
"Program received signal SIGSEV, Segmentattion fault"
ing main() at seg.c:7
“线程同步”指的是在多线程的环境下,有些资源可能被多线程同时访问,当有一个线程对这段内存地址进行操作时,其他线程都不可以对这段内存进行操作,直到该线程完成操作。
此时其他线程都处于 等待 状态(进程三态Wait)。
实现线程同步的方法有很多,临界区对象就是其中一种。
线程同步方法包括:
mutex() / wait() / signal() / notify() / notifyAll();
pthread_cond_signal() / pthread_cond_broadcast
计算机系统的各种内存管理策略,例如“分页”、“分段”等,所有这些策略都有相同的目标,就是同时将多个进程保存在内存中,以便允许多道程序。然而,这些策略都倾向于要求每个进程在执行之前应完全处于内存中(然而这是不切实际的,内存的大小是很有限的)。
“虚拟内存技术”允许执行进程不必完全处于内存中。 ------ 根本作用
这方案的一个主要优点就是:程序可以大于物理内存。 此外,虚拟内存将内存抽象成一个巨大的、同一的存储数组,进而实现了用户看到的逻辑内存与物理内存的分离。这种技术使得程序员不再担忧内存容量的限制。
虚拟内存还允许进程轻松共享文件和实现共享内存。 ------ 次要作用
此外,它为创建进程提供了有效的机制。然而,虚拟内存的实现并不容易,并且使用不当还可能会大大降低性能。
http://c.biancheng.net/view/1270.html
Connection: keep-alive
,HTTP响应头中也会加入 Connection: keep-alive
,这样才算建立起了HTTP长连接;keepalive_timeout
参数,用来指定HTTP长连接的超时时间,超时时间内未收到对端数据则会释放连接;SO_KEEPAIVE
显式开启,默认下TCP保活机制是关闭的,也就是说此时TCP将无法主动发现一条连接是否出现异常。 TCP的保活时间超时2小时。https://mp.weixin.qq.com/s/wB00GWeo66rSyLDgA-MKfw
方法一:netstat
netstat -anp | grep 12345
netstat命令用于查看网络连接和端口状态。
方法二:lsof
lsof -i:12345
lsof含义为“列出打开文件”(lists of open files),而在Unix中由于一切设备皆文件,所以losf这个命令格外的强大,支持众多的选项。其中用于查看网络端口的命令是 lsof -i:端口号
。
GET方法的含义是请求从服务器获取资源,这个资源可以是静态的文本、页面、图片视频等,例如:
//请求:
GET /index.html HTTP/1.1
Host: www.coding.com
//响应:
返回 index.html 的页面资源
而POST方法则相反的,它向URI指定的资源提交数据,数据就放在 body
里,例如:
//请求:
POST /index.html HTTP/1.1
Host: www.coding.com
//响应:
返回服务器处理的结果
GET方法是安全且幂等的,POST不是安全且不是幂等的。
因为GET方法是“只读”操作,POST是“新增或提交数据”的操作,会修改服务器上的资源。
安全和幂等的概念:
https://www.cnblogs.com/xiaolincoding/p/12442435.htm
四条,影响程度依次递减:
业界普遍的做法是:
两阶段提交。