第I部分:C++基础-第2-3章

179 阅读4分钟

变量和基本类型

算术类型

数据类型最小尺寸补充
bool1 byte布尔类型
char1 byte字符
wchar_t2 byte宽字符
wchar16_t2 byteUnicode字符
wchar32_t4 byteUnicode字符
short2 byte短整型
int4 byte整型
long long8 byte长整型
float4 byte,6 位有效数字浮点型
double4 byte,10 位有效数字浮点型

long类型所占字节数和平台有关,如VS2019中为4bytes,而linux64中为8bytes, long double也有类似问题. float小数可能发生进位,但不是简单按照四舍五入的.

这里引出结构体字节对齐问题,如何结算结构体所占字节数.

结构体内存对齐

为何要内存对齐

假设内存不对齐,a为char类型占一个字节,b为int类型占4个字节,内存排布如下:

从内存读取数据一般是一个个内存块内读取的,每次读取一般都是4bytes的整数倍. 若内部不对齐,假设每次读取4bytes,为了读取a,b,需要读取a的1个字节,b的前3个字节,那么读取b就要分两次,最后还要拼接组合..\Rightarrow读取效率低

结构体如何对齐

  1. 成员1在结构体偏移位0的地址处(首地址)
  2. 其他成员var对齐到N的整数倍地址上(N=min(sizeof(var),k)N = min(sizeof(var),k),k为指定对齐的值,64位机器上一般为8)
  3. 结构体大小 % 最大对齐数(所有成员(2)中的) == 0
  4. 嵌套结构体对齐到自己的最大对齐整数倍出,结构体总大小 % 最大对齐数 == 0

最后一条,计算出变量在内存中最大地址偏移量对应的字节数,然后调整为最大对齐数的整数倍.

struct B {
	char x;            //y---|xxxx|        -->8
	int y = 1;       
};

struct A {
	char a;           //a---|y---|xxxx|    -->12
	struct B tmp;
	
};

struct C {
	char a;              //ay--|xxxx|       -->8
	char x;
	int y = 1;     
};

int main()
{
	cout << sizeof(B) << endl;    //8
	cout << sizeof(A) << endl;   //12
	cout << sizeof(C) << endl;   //8
	return 0;
}

image.png

类型转换

类型1 var = 数据(类型2)

int main()
{
    unsigned char a = -1;
    unsigned char b = 255;
    unsigned char c = 256 + 97;
    unsigned char d = 97;
    cout<<a<<endl;
    cout<<b<<endl;
    cout<<c<<endl;
    cout<<d<<endl;
    return 0;
}

unsigned char表示无符号数,可以表示0-255区间的值, -1不属于该区间,256+var256\frac{|256+var|}{256}

实际使用要避免混用带符号数据和无符号数据 image.png

一个易错点: 在for循环中,下面这种写法不会得到想要的效果,因为你当j等于0时, j - 1得到4294967295,循环将无法终止.

for(unsigned j = 5;j>=0;--j){
     cout<<j<<endl;
}

建议:第一次使用变量时再定义

  • 方便找,赋值方便
  • 程序可能提前退出,后面定义就不需要,节约资源

复合类型: 指针和引用

C++11增加了右值引用,一般提引用说的是左值引用.

引用: 别名,给已经存在的对象起的另一个名字 特性:

  • 必须被初始化,之后一直和初始化对象绑定,无法重新绑定到另一个对象.
  • 可以通过引用更改对象

指针 : 一个变量,存放另一个变量的地址

int a;
int &b = a;
int *p;

与引用区别:

  • 无需赋初值,当然这样会拥有一个不确定的值
  • 可以更改指向对象
  • sizeof得到结果不一样,64位机器上,指针类型得到的是8个字节,而引用取决于绑定对象大小
  • ++含义不一样,p++表示指向后面一部分内存,b++表示a++.

关键字,限定符

extern 关键字

分开变量定义和声明,若extern语句指定了初值,就变成了定义

extern int a;  //声明
int b ;    //定义
extern int c = 2; //定义

用途: 分离式编译使用同一变量 如test.cpp中定义全局const变量num, int num = 5, main.cpp中需要用到这个变量,方法有:

  1. include包含这个文件
  2. 在main.cpp中声明: extern int num,然后使用它. (命令行编译 gcc -o main main.cpp test.cpp -Wall )

当然这样可能导致在main.cpp中更改了变量num,所以在test.cpp,和main.cpp中都加上const修饰,看起来一切完美,再编译发生如下错误: undefined reference to num . 原来在C++11中,当变量使用const修饰而又没有使用extern修饰,且此前没有被声明为external linkage时,它是internal linkage,若想使用,则需要在test.cpp中修改为:

extern const int num = 5;

const关键字

作用: 限定对象只读.

const对象创建后不能更改,所以const对象必须被初始化. 默认情况下,const对象只在文件内有效(和前面所说一样,想用就在定义时加extern)

cosnt引用

对const对象的引用,修饰后不能通过引用更改对象的值.但绑定对象是可以通过其他方式改变的.

int a = 42;
int &b = a;
const int &c = a;
cout<<c<<endl;   // c = 42
b = 4;
cout<<c<<endl;  // c = 4

对象是常量,引用或者指针也必须用const修饰.

const指针

const *ptr: 指向常量的指针,不能通过指针修改值

*const ptr: 常量指针,指针指向不能修改

int a = 3;

int *const ptr1 = &a;  // 指针是常量
int const *ptr2 = &a;  //
const int * const ptr3  = &a;

int b = 5;
//ptr1 = &b;  // error
*ptr1 = 3;
cout<<a<<endl;  // 3

ptr2 = &b;
//*ptr2 = 5;  //error

//ptr3 = &b;    // error
// *ptr3 = 6;    // error

顶层const和底层const: 顶层const表示指针本身是常量,底层const表示指针所指对象是常量.

constexpr

C++11赋予const的是只读属性,而constexpr表示后面是常量或者常量表达式,编译期可以计算出来.

const int a = 5;
int *p = (int*)&a;
*p = 8;
cout<<a<<endl;  
cout<<*p<<endl;
cout<<&a<<endl;
cout<<p<<endl;

以上程序中,C++执行结果显示a = 5,*p = 8,二者地址一样.而c语言中,二者值都变成8. 地址一样值不一样怎么解释呢? 下面查看C++,c程序对应的汇编代码

C++

	cout << a << endl;
00BA1BB5  mov         esi,esp  
00BA1BB7  push        5  
...

C

	printf("&a=%x\n", &a);
00D01885  lea         eax,[a]  
00D01888  push        eax  
00D01889  push        offset string "&a=%x\n" (0D07B30h)  
00D0188E  call        _printf (0D010CDh)  
00D01893  add         esp,8  

由此可知:差异是编译器优化造成的,通过p的确改变了a的值,但是C++里面a值读了一次,后面再用到都是用它对应的常量替换,而没有从地址处重新读取. 而c语言里面则重新读取了a的地址.

const和constexpr的具体使用的不同:

constexpr int sqr1(int arg){
    return arg*arg;
}

const int sqr2(int arg){
    return arg*arg;
}

int main()
{
    array<int,sqr1(10)> mylist1;  //可以,因为sqr1时constexpr函数
    array<int,sqr2(10)> mylist1;   //不可以,因为sqr2不是constexpr函数
    return 0;
}

auto和decltype

auto

auto推导一般忽略顶层const,保留底层const,看下例:

const int a = 4;
auto c = a;  // 取变量名在顶层const
c = 5;
cout<<c<<endl;  // 5

auto b = &a;  //取地址保留了底层const
b = 3;   // error

decltype 推导表达式类型定义变量,但不想用它的结果初始化,就用decltype

int a = 3;
int b = 1;
decltype(a+b) c = 3.14;
cout<<c<<endl;   // 3

decltype和引用

auto不同的是,decltype会返回变量的完整类型(包括引用,顶层const)

decltype(expr)中expr如果是解引用指针,如*p,那么得到的就是引用类型.

若为双层括号,decltype((expr))得到的一定是引用.

字符串

c风格的字符串

c风格的字符串以'\0'结束,看程序:

        char arr1[] = { 'a','b','c','d' };
	cout << strlen(arr1) << endl;   //不确定值

	char arr2[] = { 'a','b','c','d','\0'};
	cout << strlen(arr2) << endl;  // 4

第一个字符串数组不是\0结束,所以用strlen求长度会继续往后找,直到遇到\0,所以结果有误, 第二种求长度会忽略\0

与c代码接口

C++里面使用string,而因历史原因可能使用了c风格字符串,直接传递string类型肯定不行,于是有c_str()接口,但注意有陷阱:

string s = "abc";
const char *str = s.c_str();
s =  "bbb";
cout<<str<<endl;   // bbb

c_str()返回的是const char*类型,但后面原始的string对象改变了,字符数组s会变.

实现一个简单的string

MyString.h

#pragma once
#include <cstring>
#include <iostream>


using std::ostream;

class String {
	friend ostream& operator<<(ostream& os, const String& rhs);
public:
	String(const char* rhs = nullptr);
	String(const String& rhs);
	String& operator=(const String& rhs);
	String(String&& rhs) noexcept;
	~String();
public:
	size_t size() const;
private:
	char* str;
	size_t len;
private:
	void init(const char* s);
};

MyString.cpp

#include "MyString.h"

String::String(const char* rhs /*= nullptr*/)
{
	init(rhs);
}

size_t String::size() const
{
	return len;
}

void String::init(const char* s)
{
	if (s == nullptr) {
		str = nullptr;
		len = 0;
	}
	else {
		len = strlen(s);
		str = new char[len + 1];//确保最后一个字符是\0
		//str[len] = '\0';
		strcpy(str, s);
	}
}

ostream& operator<<(ostream& os, const String& rhs)
{
	if (rhs.str == nullptr) {
		os << "";
	}
	else {
		os << rhs.str;
	}
	return os;
}

String::String(const String& rhs) //String S(rhs)
{
	//String(rhs.str);  //相当于调用了构造函数,即生成了一个新对象
	init(rhs.str);
}

String& String::operator=(const String& rhs)  // str = str 这种也要考虑到
{
	if (this != &rhs) {
		delete[] str;
		init(rhs.str);
	}
	return *this;
}



String::String(String&& rhs) noexcept // s(s) 也要考虑到
{
	if (this != &rhs) {
		delete str;

		str = rhs.str;
		len = rhs.len;

		rhs.str = nullptr;
		rhs.len = 0;
	}
}


String::~String()
{
	if (str != nullptr || len != 0) {
		std::cout << "destructor finished!\n";
		delete[] str;
		len = 0;
	}
	//std::cout << "destructor finished!\n";
}

main.cpp测试

#include<iostream>
#include "MyString.h"

using namespace std;



int main()
{
	{
		String str1 = "Hello";
		cout << str1 << endl;
		cout << str1.size() << endl;

		String str2(str1);
		cout << str2 << endl;
		cout << str2.size() << endl;

		str2 = "Good";
		String str3 = str2;
		cout << str3 << endl;
		cout << str3.size() << endl;


		str3 = "ABC";
		String str4(std::move(str3));
		cout << str3 << endl;
		cout << str3.size() << endl;
		cout << str4 << endl;
		cout << str4.size() << endl;

		String str5("World!");
		cout << str5 << endl;
		cout << str5.size() << endl;

		str5 = "world";
		str2 = str5;   
		cout << str5 << endl;
		cout << str5.size() << endl;
	}
	system("pause");
	return 0;
}