2021年5月29日 星期六

scanf 為什麼要一定要加&

scanf 為什麼要一定要加&

tags: 部落格文章

相信大家從一開始剛學程式的時候一定都踩過這個坑,沒有加&導致程式錯誤,找半天還找不到那種的。

為什麼要加最主要的原因是,透過地址的傳遞才能夠修改變數。



& 是什麼意思

簡單說就是獲取位址的意思。

所謂的變數,其實可以理解成位址的別名,就比如說你在地圖上搜尋臺北車站 和搜尋 台灣台北市中正區北平西路3號100臺灣 可以找到同一棟建築物是一樣意思。

下面的代碼演示一下結果

int TaipeiStation;
cout << "台北車站的地址是:" << &TaipeiStation << endl;

利用地址修改變數

而其中位址這個型態,可以在宣告的時候加個 * 號來儲存。

int  num;

//宣告的時候星號表示這個變數是一個指標
int* p = &num;

現在你可以透過別名 num 來修改記憶體中的數值

num = 10;

也可以透過地址來修改

// 使用的時候星號表示讀取地址中的數值
*p = 10;

透過函式修改變數

現在有了位址之後就可以把地址傳進函式了

void increase(int* num){
    num++;
}

使用的話就像這個樣子

int num;
increase(&num);

現在謎底就揭曉拉,就是因為這個原因才導致一定要加上取址符號的



如果不打取址符號會發生什麼

其實 scanf 只要是傳遞位址進去就可以,並不是說一定必須,只是加上才是正確的寫法。如果不是傳遞位址的話,程式也會自動把數據轉換成位址。

傳遞整數

試著直接把一個整數傳入scanf

int i;
scanf("%d", i);

上面的情況會把i傳進去,對於scanf來說就是會試圖去讀取i這個位址有什麼東西,但是i沒有給初始值,沒有給初始值得話預設會是一個垃圾值,垃圾值可以當作一個亂數,產生的原因是上一個程式用完沒清掉的結果

嘗試對於亂數的位址存取是非法的,所以會報錯

Visual Studio 對於未初始的變數是不給使用的,這段在上面會直接報錯


傳遞整數位置

剛剛的錯誤在於位址是非法的位址,現在我們嘗試傳遞一個合法的地址進去就可以執行了

int p;
int i=(int)(&p);
cout << "[HEX:" << &p << "], [DEC:" << (int)&p << "]" << endl;

scanf("%d", i);

printf("%d\n", p);
printf("%d\n", *((int*)i));

i是一個 int,會被隱式轉換成 int* 接著scanf就會對轉換出來的位址存取,如果是合法的位址那就不會有錯誤。

2021年5月23日 星期日

寫C/C++測試題目時如何讀取測資

寫C/C++測試題目時如何讀取測資

tags: 部落格文章

以前寫測試題目的時候最麻煩的第一步就是如何讀測資了

方法有很多cin/cout是最簡單的,但是相對的會比較費時,建議是盡量避開全部都使用 printf 和 scanf。

再來第二個問題是,在本地測試的時候會把測資寫到檔案裡面讀取,但是上傳之後是直接從stdin輸入,導致上傳的時候還要修改一下,

這邊也一併提出解決辦法,整理一下我認為最佳的解法。

說明

先說明如何讀取的代碼

char buff[32];
while (scanf("%s", buff)!=EOF) {
    printf("%s\n", buff);
}

直接用一個while包起來,代碼從裡面開始寫就可以了。

再來是解決上傳修改代碼的問題使用這個函式,可以直接修改scanf讀取檔案,而不是從stdin讀取,這樣代碼就不用修改了

freopen("input.txt", "r", stdin);   // scanf從檔案讀取
freopen("output.txt", "w", stdout); // printf 輸出到檔案

因為函式會略微影響效能,應該有不少人是用define去包起來的

#define ONLINE_JUDGE

#ifndef ONLINE_JUDGE
    if(in) freopen("input.txt", "r", stdin);
    if(out) freopen("output.txt", "w", stdout);
#endif // ONLINE_JUDGE

如此一來只要註解掉第一行的 define 中間代碼就不會被執行了
不過這邊推薦一個更好的方式,用 inline 函式,效果跟 define 是一樣的

inline void debugMode() {
    if(in) freopen("input.txt", "r", stdin);
    if(out) freopen("output.txt", "w", stdout);
}

加上 inline 的意思是,如果函式符合一定的規則(簡單說就是足夠簡單),編譯器會自動把函式打掉,直接開到呼叫的地方。就等於沒有呼叫函式的意思了,所以速度會快一點。

如果有其他需要寫到函式建議都要加上 inline ,可以省下一點點時間。

完整的範例

#include <cstdio>
#include <cstdlib>

inline void debugMode(const char* in, const char* out) {
    if(in) freopen(in, "r", stdin);
    if(out) freopen(out, "w", stdout);
}

int main(int argc,char *argv[]) {
    debugMode("in.txt", NULL); // 輸出保持在終端機

    for (char buff[32]; scanf("%s", buff)!=EOF; ) {
        printf("%s\n", buff);
    }
}

參考

 

2021年5月19日 星期三

C/C++ 依照特定格式 讀取檔案並 切割字串

C/C++ 依照特定格式 讀取檔案並 切割字串

tags: 部落格文章

這個新手大概很常用到,每次要用都google半天。這篇是把這些常用的函式封裝成一個物件,直接解決痛點方便操作。

因為是用 C++ 17 寫的,刷題提交用的代碼可能用不上,網站未必支援。簡單的讀檔可以參考這邊兩篇站內文。

讀取檔案

讀檔的問題參照上面兩篇就可以解決,這邊就概略貼上代碼就好

// 逐行讀取
vector<string> ReadFile_line(const string file_name) {
    vector<string> v;
    fstream fs(file_name, ios::in);
    if (!fs.is_open())
        throw runtime_error("Reading error.");
    for (string str; getline(fs, str);)
        v.emplace_back(str);
    return v;
}



切割字串

傳統的C字串處理可以這樣寫

#include <string.h>
void stringSplit(char* str, const char* delim) {
    char temp[64];
    strcpy(temp, str);

    char* pch = strtok(temp, delim);
    while (pch != NULL) {
        printf("%s\n", pch);
        pch = strtok(NULL, delim);
    }
}

C++ 11 可以這樣寫

#include <string>
void split(const string& s, vector<string>& tokens,
    const string& delimiters = " ")
{
    string::size_type lastPos = s.find_first_not_of(delimiters, 0);
    string::size_type pos = s.find_first_of(delimiters, lastPos);
    while (string::npos != pos || string::npos != lastPos) {
        tokens.push_back(s.substr(lastPos, pos - lastPos));
        lastPos = s.find_first_not_of(delimiters, pos);
        pos = s.find_first_of(delimiters, lastPos);
    }
}

C++17 可以更有效率的這樣寫

#include <string_view>
vector<string_view> splitSV(string_view strv, string_view delims = " ") {
    vector<string_view> output;
    for (size_t first = 0; first < strv.size();) {
        const auto second = strv.find_first_of(delims, first);
        if (first != second)
            output.emplace_back(strv.substr(first, second - first));
        if (second == string_view::npos)
            break;
        first = second + 1;
    }
    return output;
}

透過這樣的方式,就能夠把字串依照特定格式給切出來了。



封裝物件

切割完畢之後就用封裝拉,一般就是用 vector<string> 封裝,想要效率更高一點可以使用 vector<string_view>

因為有點長就貼到gist上了

封裝好的物件怎麼使用有一併寫在檔案底下的test函式裡

下面就簡單說明一下,最簡單的切割

string str = "123 | 321";
OneLine line(str, " | ");
cout << line << endl;

vector<string_view>&& tokenList = line;
for (size_t i = 0; i < tokenList.size(); i++)
    cout << tokenList[i] << endl;

物件本身返回的是一個 vector
這就意味了著在for裡面可以透過 line[idx] 來讀取被切割的字串

如果要從檔案讀取可以使用 openFile()

OneLine line;
line.openFile("a.txt");
while (line.getline()) {
    vector<string_view>&& tokenList = line;
    cout << tokenList << endl;
}

此時獲取的 tokenList 就可以用下標取出個別的字串了。



參考

Win10/11 變更使用者目錄的路徑 (中文改英文)

Win10/11 變更使用者目錄的路徑 (中文改英文)

tags: 部落格文章

怕過程出錯影響到使用者資料,可以先先創建第二個中文姓名的使用者充當白老鼠試驗,避免出什麼狀況。這不是必須的,不想弄第二次就直接用本尊來吧。

創建好之後記得先登入一次,不然使用者資料是空的還沒初始化




獲取使用者UID

先用要更改的使用者,打開終端機輸入以下命令獲取UID信息
(打開終端機方法:按下 Win+X 然後再按 A。或是直接搜尋Powershell打開)

[Security.Principal.WindowsIdentity]::GetCurrent().User.Value

會得到一串數字,把結尾幾位數記下來等一下會用到。




登入第二個管理員使用者

同一個帳號無法在檔案執行中修改目錄名稱,需要第二個普通管理員使用者來操作。如果電腦上沒有第二個使用者可以暫時打開超級管理,設定完後要刪除比較方便。


1. 打開超級管理員

先按下 Win+X 然後再按下 A ➔ 打開終端機,接著再輸入以下的代碼

net user "Administrator" /active:yes

2. 然後重新啟動電腦,選擇使用Administor(超級管理員)帳號登入

* 如果你的電腦重啟後自動登入原本的使用者。請記得登出後再選 Administor 登入



變更帳號路徑

再來進入超級管理員帳號就有權限可以做事情了


1. 打開登錄檔編輯器 (在開始搜尋 regedit 可以找到)




2. 找到下面這條路徑 (可以像資料夾那樣直接貼在上方快速到達)

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Profilelist



3. 修改登錄檔與資料夾名稱

這邊會看到很多資料夾 S-1-XXXX 這種的

  • 其中一個是你的使用者文件,剛剛有把數字記下來就直接對照就好
  • 沒有記下來就一個一個點進去,點到有熟悉的中文路徑就是對了


裡面可以看到一個文件是 ProfileImagePath 這個就是資料夾所在位置了。

  1. 登錄檔管理器中雙擊 ProfileImagePath 確認資料夾名稱


  2. 打開檔案總管,上方路徑貼上 C:\Users 並按下 Enter 前往該路徑


  3. 變更使用者文件的資料夾名字


  4. 雙擊進入變更後的使用者文件,並從上方複製路徑


  5. 回登錄檔管理器,把變更後的路徑貼回 ProfileImagePath 項目上


請務必使用複製貼上的方式更新路徑。



4. 確保萬無一失再重啟

重開前務必確保以下項目

  1. 使用者資料夾改名了
  2. 登錄檔 ProfileImagePath 的數值改了
  3. 上面兩個路徑是吻合的


再來重新啟動電腦,並登入變更後帳號即可。

* 以上只是改路徑,使用者名稱還是原來的使用者名稱沒有變的。



5. 關閉超級管理員

確保能正常登入後就可以把超級管理員關掉了,執行以下的代碼關閉。
net user "Administrator" /active:no







2021年5月12日 星期三

PowerShell 修改檔案的 建立時間/修改時間/存取時間 程式

PowerShell 修改檔案的 建立時間/修改時間/存取時間 程式

tags: 部落格文章


新文章有更易用的方式詳細請看這篇https://charlottehong.blogspot.com/2022/02/powershell.html







關於檔案的 修改時間 和 存取時間有什麼差別可以看這篇站內文
https://charlottehong.blogspot.com/2021/05/win10.html

字串格式和日期格式怎麼互轉可以參考這一邊
https://charlottehong.blogspot.com/2021/05/powershell-string-datetime.html

修改檔案時間

其實也就是讀檔然後修改而已。

$Date = New-Object DateTime(2021, 12, 31, 18, 30, 59);
$FileName = "Z:\a.txt"

$file = Get-Item $FileName
$file.CreationTime   = $Date; #建立日期
$file.LastWriteTime  = $Date; #修改日期
$file.LastAccessTime = $Date; #存取日期

懶人包程式

這是封裝好的程式,要使用的話按下 Win+X 在按 A 會跳出一個可以輸入的視窗,把底下全部貼上。

# 產生日期格式
function NewDatetime {
    param (
        [string]$date = "",
        [string]$formatType = "yyyy-MM-dd HH:mm:ss"
    )
    # 未輸入日期則返回當前時間
    if ([string]::IsNullOrEmpty($date)) {
        $newDate = (Get-Date)
    }
    # 格式化日期
    else {
        $newDate = [DateTime]::ParseExact(
            $date.Trim(), $formatType,
            [CultureInfo]::InvariantCulture
        )
    }
    return $newDate
}
# 讀取檔案日期
function readFileDate {
    param (
        [string]$FileName
    )
    $file = Get-Item $FileName;
    Write-Host "建立日期" $file.CreationTime;     #建立日期
    Write-Host "修改日期" $file.LastWriteTime;    #修改日期
    Write-Host "存取日期" $file.LastAccessTime;   #存取日期
}
# 改變檔案日期
function ChangeFileDate {
    param (
        # [Parameter(Mandatory, Position=0)]
        [Parameter(Mandatory, Position=0)]
            [string]$FileName,
        [Parameter(ParameterSetName="setDetail")]
            [datetime]$CreationTime,
        [Parameter(ParameterSetName="setDetail")]
            [datetime]$LastWriteTime,
        [Parameter(ParameterSetName="setDetail")]
            [datetime]$LastAccessTime,
        [Parameter(ParameterSetName="setAll")]
            [datetime]$AllTime,
        [switch]$Preview
    )
    if ($AllTime) {
        $CreationTime   = $AllTime;
        $LastWriteTime  = $AllTime;
        $LastAccessTime = $AllTime;
    }

    $file = Get-Item $FileName
    if ($Preview) {
        Write-Host "[$FileName]"
        Write-Host "  " $file.CreationTime "-->" $CreationTime
        Write-Host "  " $file.LastWriteTime "-->" $LastWriteTime
        Write-Host  "  " $file.LastAccessTime "-->" $LastAccessTime
    } else {
        if ($CreationTime) {
            $file.CreationTime = $CreationTime; #建立日期
        } if ($LastWriteTime) {
            $file.LastWriteTime = $LastWriteTime; #修改日期
        } if ($LastAccessTime) {
            $file.LastAccessTime = $LastAccessTime; #存取日期
        }
    }
}

全部貼上去之後按 Enter 就可以開始使用了



使用方法

在使用前先執行這兩行代碼(記得要按Ennter輸入)
會自動切換到桌面,並建一個檔案等一下要範例用

cd ~\Desktop
"" >> a.txt

好了就可以開始拉

查詢檔案日期

readFileDate a.txt

修改全部日期

ChangeFileDate "a.txt" -AllTime "2021-5-10 23:59:59"

改完之後按 ↑ 兩次選到剛剛執行查詢的代碼,可以看一下有沒有改變

修改單一日期

如果只想改變其中一個而已的話,改法像這樣

# 只改變建立日期
ChangeFileDate "a.txt" -CreationTime "2021-5-11 23:59:59"
# 只改變修改日期
ChangeFileDate "a.txt" -LastWriteTime "2021-5-12 23:59:59"
# 只改變存取日期
ChangeFileDate "a.txt" -LastAccessTime "2021-5-13 23:59:59"

如果要改其中兩個,可以打兩行也可以把後面的部分串起來就好

# 改任意兩個(這3組可以隨便串)
ChangeFileDate "a.txt" -CreationTime "2021-5-14 23:59:59" -LastAccessTime "2021-5-15 23:59:59"

預覽

如果你怕改錯的話可以在加上 -Preview 預覽一下
加上這個之後就不會變更日期,只會看到預覽的訊息

ChangeFileDate "a.txt" -AllTime "2021-5-10 23:59:59" -Preview

PowerShell 字串String 與 日期Datetime 格式轉換

PowerShell 字串String 與 日期Datetime 格式轉換

tags: 部落格文章

字串轉時間

建構子

直接用建構子是最簡單的

New-Object DateTime(2021, 5, 12, 22, 29, 30)


進階自訂格式

核心的程式長這樣

[DateTime]::ParseExact("13.May.2017", "dd.MMM.yyyy",[CultureInfo]::InvariantCulture)

這樣簡單一行就能夠格式化字串到日期了

強化一下寫好的副程式是這樣的

function newDatetime {
    param (
        [string]$date = "",
        [string]$formatType = "yyyy-MM-dd HH:mm:ss"
    )
    # 未輸入日期則返回當前時間
    if ([string]::IsNullOrEmpty($date)) {
        $newDate = (Get-Date)
    }
    # 格式化日期
    else {
        $newDate = [DateTime]::ParseExact(
            $date.Trim(), $formatType,
            [CultureInfo]::InvariantCulture
        )
    }
    return $newDate
}

使用的話直接這就可以用了

newDatetime "2000-01-01 12:00:00"

要自訂格式的話,後面空格多補一個參數

newDatetime "13.May.2017" "dd.MMM.yyyy"



日期轉字串

再來是把日期轉出來到字串,這個就比較容易

方式1

直接轉成字串即可

(Get-Date).ToString("yyyy-MM-dd HH:mm:ss")

方式2

陣列轉換

'{0:yyyy-MM-dd HH:mm:ss}' -f (Get-Date)